mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-14 12:23:52 +02:00
1897 lines
95 KiB
Vue
1897 lines
95 KiB
Vue
<template>
|
|
<div>
|
|
<DialogHeader class="sr-only">
|
|
<DialogTitle>{{ groupDialog.ref?.name || t('dialog.group.info.header') }}</DialogTitle>
|
|
<DialogDescription>
|
|
{{ groupDialog.ref?.description || groupDialog.ref?.name || t('dialog.group.info.header') }}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div>
|
|
<div style="display: flex">
|
|
<img
|
|
:src="groupDialog.ref.iconUrl"
|
|
style="flex: none; width: 120px; height: 120px; border-radius: 12px"
|
|
class="cursor-pointer"
|
|
@click="showFullscreenImageDialog(groupDialog.ref.iconUrl)"
|
|
loading="lazy" />
|
|
<div style="flex: 1; display: flex; align-items: center; margin-left: 15px">
|
|
<div class="group-header" style="flex: 1">
|
|
<span v-if="groupDialog.ref.ownerId === currentUser.id" style="margin-right: 5px">👑</span>
|
|
<span
|
|
class="font-bold"
|
|
style="margin-right: 5px; cursor: pointer"
|
|
v-text="groupDialog.ref.name"
|
|
@click="copyToClipboard(groupDialog.ref.name)"></span>
|
|
<span
|
|
class="group-discriminator x-grey"
|
|
style="font-family: monospace; font-size: 12px; margin-right: 5px">
|
|
{{ groupDialog.ref.shortCode }}.{{ groupDialog.ref.discriminator }}
|
|
</span>
|
|
<TooltipWrapper v-for="item in groupDialog.ref.$languages" :key="item.key" side="top">
|
|
<template #content>
|
|
<span>{{ item.value }} ({{ item.key }})</span>
|
|
</template>
|
|
<span
|
|
class="flags"
|
|
:class="languageClass(item.key)"
|
|
style="display: inline-block; margin-right: 5px"></span>
|
|
</TooltipWrapper>
|
|
<div style="margin-top: 5px">
|
|
<span
|
|
class="cursor-pointer x-grey"
|
|
style="font-family: monospace"
|
|
@click="showUserDialog(groupDialog.ref.ownerId)"
|
|
v-text="groupDialog.ownerDisplayName"></span>
|
|
</div>
|
|
<div class="group-tags">
|
|
<Badge
|
|
v-if="groupDialog.ref.isVerified"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.verified') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="groupDialog.ref.privacy === 'private'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.private') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="groupDialog.ref.privacy === 'default'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.public') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="groupDialog.ref.joinState === 'open'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.open') }}
|
|
</Badge>
|
|
<Badge
|
|
v-else-if="groupDialog.ref.joinState === 'request'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.request') }}
|
|
</Badge>
|
|
<Badge
|
|
v-else-if="groupDialog.ref.joinState === 'invite'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.invite') }}
|
|
</Badge>
|
|
<Badge
|
|
v-else-if="groupDialog.ref.joinState === 'closed'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.closed') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="groupDialog.inGroup"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.joined') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="groupDialog.ref.myMember && groupDialog.ref.myMember.bannedAt"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.banned') }}
|
|
</Badge>
|
|
<template v-if="groupDialog.inGroup && groupDialog.ref.myMember">
|
|
<Badge
|
|
v-if="groupDialog.ref.myMember.visibility === 'visible'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.visible') }}
|
|
</Badge>
|
|
<Badge
|
|
v-else-if="groupDialog.ref.myMember.visibility === 'friends'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.friends') }}
|
|
</Badge>
|
|
<Badge
|
|
v-else-if="groupDialog.ref.myMember.visibility === 'hidden'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.hidden') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.group.tags.subscribed') }}
|
|
</Badge>
|
|
</template>
|
|
</div>
|
|
<div style="margin-top: 5px">
|
|
<pre
|
|
v-show="groupDialog.ref.name !== groupDialog.ref.description"
|
|
style="
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
max-height: 40vh;
|
|
overflow-y: auto;
|
|
"
|
|
v-text="groupDialog.ref.description"></pre>
|
|
</div>
|
|
</div>
|
|
<div style="flex: none; margin-left: 10px">
|
|
<template v-if="groupDialog.inGroup && groupDialog.ref?.myMember">
|
|
<TooltipWrapper
|
|
v-if="groupDialog.ref.myMember?.isRepresenting"
|
|
side="top"
|
|
:content="t('dialog.group.actions.unrepresent_tooltip')">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
variant="secondary"
|
|
size="icon-lg"
|
|
style="margin-left: 5px"
|
|
@click="clearGroupRepresentation(groupDialog.id)">
|
|
<BookmarkCheck />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-else side="top" :content="t('dialog.group.actions.represent_tooltip')">
|
|
<span>
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
variant="outline"
|
|
size="icon-lg"
|
|
:disabled="groupDialog.ref.privacy === 'private'"
|
|
@click="setGroupRepresentation(groupDialog.id)">
|
|
<Bookmark />
|
|
</Button>
|
|
</span>
|
|
</TooltipWrapper>
|
|
</template>
|
|
<template v-else-if="groupDialog.ref.myMember?.membershipStatus === 'requested'">
|
|
<TooltipWrapper side="top" :content="t('dialog.group.actions.cancel_join_request_tooltip')">
|
|
<span>
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
variant="outline"
|
|
size="icon-lg"
|
|
@click="cancelGroupRequest(groupDialog.id)">
|
|
<X />
|
|
</Button>
|
|
</span>
|
|
</TooltipWrapper>
|
|
</template>
|
|
<template v-else-if="groupDialog.ref.myMember?.membershipStatus === 'invited'">
|
|
<TooltipWrapper side="top" :content="t('dialog.group.actions.pending_request_tooltip')">
|
|
<span>
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
variant="outline"
|
|
size="icon-lg"
|
|
@click="joinGroup(groupDialog.id)">
|
|
<Check />
|
|
</Button>
|
|
</span>
|
|
</TooltipWrapper>
|
|
</template>
|
|
<template v-else>
|
|
<TooltipWrapper
|
|
v-if="groupDialog.ref.joinState === 'request'"
|
|
side="top"
|
|
:content="t('dialog.group.actions.request_join_tooltip')">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
variant="outline"
|
|
size="icon-lg"
|
|
@click="joinGroup(groupDialog.id)">
|
|
<MessageSquare />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper
|
|
v-if="groupDialog.ref.joinState === 'invite'"
|
|
side="top"
|
|
:content="t('dialog.group.actions.invite_required_tooltip')">
|
|
<span>
|
|
<Button class="rounded-full mr-2" variant="outline" size="icon-lg" disabled>
|
|
<MessageSquare />
|
|
</Button>
|
|
</span>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper
|
|
v-if="groupDialog.ref.joinState === 'open'"
|
|
side="top"
|
|
:content="t('dialog.group.actions.join_group_tooltip')">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
variant="outline"
|
|
size="icon-lg"
|
|
@click="joinGroup(groupDialog.id)">
|
|
<Check />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
</template>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger as-child>
|
|
<Button
|
|
class="rounded-full"
|
|
:variant="
|
|
groupDialog.ref.membershipStatus === 'userblocked' ? 'destructive' : 'outline'
|
|
"
|
|
size="icon-lg">
|
|
<MoreHorizontal />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem @click="groupDialogCommand('Refresh')">
|
|
<RefreshCw class="size-4" />
|
|
{{ t('dialog.group.actions.refresh') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="groupDialogCommand('Share')">
|
|
<Share2 class="size-4" />
|
|
{{ t('dialog.group.actions.share') }}
|
|
</DropdownMenuItem>
|
|
<template v-if="groupDialog.inGroup">
|
|
<template v-if="groupDialog.ref.myMember">
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements"
|
|
@click="groupDialogCommand('Unsubscribe To Announcements')">
|
|
<BellOff class="size-4" />
|
|
{{ t('dialog.group.actions.unsubscribe') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
v-else
|
|
@click="groupDialogCommand('Subscribe To Announcements')">
|
|
<Bell class="size-4" />
|
|
{{ t('dialog.group.actions.subscribe') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
v-if="hasGroupPermission(groupDialog.ref, 'group-invites-manage')"
|
|
@click="groupDialogCommand('Invite To Group')">
|
|
<MessageSquare class="size-4" />
|
|
{{ t('dialog.group.actions.invite_to_group') }}
|
|
</DropdownMenuItem>
|
|
<template
|
|
v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')">
|
|
<DropdownMenuItem @click="groupDialogCommand('Create Post')">
|
|
<Ticket class="size-4" />
|
|
{{ t('dialog.group.actions.create_post') }}
|
|
</DropdownMenuItem>
|
|
</template>
|
|
<DropdownMenuItem
|
|
:disabled="!hasGroupModerationPermission(groupDialog.ref)"
|
|
@click="groupDialogCommand('Moderation Tools')">
|
|
<Settings class="size-4" />
|
|
{{ t('dialog.group.actions.moderation_tools') }}
|
|
</DropdownMenuItem>
|
|
<template
|
|
v-if="groupDialog.ref.myMember && groupDialog.ref.privacy === 'default'">
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem @click="groupDialogCommand('Visibility Everyone')">
|
|
<Eye class="size-4" />
|
|
<Check
|
|
v-if="groupDialog.ref.myMember.visibility === 'visible'"
|
|
class="size-4" />
|
|
{{ t('dialog.group.actions.visibility_everyone') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="groupDialogCommand('Visibility Friends')">
|
|
<Eye class="size-4" />
|
|
<Check
|
|
v-if="groupDialog.ref.myMember.visibility === 'friends'"
|
|
class="size-4" />
|
|
{{ t('dialog.group.actions.visibility_friends') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="groupDialogCommand('Visibility Hidden')">
|
|
<Eye class="size-4" />
|
|
<Check
|
|
v-if="groupDialog.ref.myMember.visibility === 'hidden'"
|
|
class="size-4" />
|
|
{{ t('dialog.group.actions.visibility_hidden') }}
|
|
</DropdownMenuItem>
|
|
</template>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
variant="destructive"
|
|
@click="groupDialogCommand('Leave Group')">
|
|
<Trash2 class="size-4" />
|
|
{{ t('dialog.group.actions.leave') }}
|
|
</DropdownMenuItem>
|
|
</template>
|
|
</template>
|
|
<template v-else>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
v-if="groupDialog.ref.membershipStatus === 'userblocked'"
|
|
variant="destructive"
|
|
@click="groupDialogCommand('Unblock Group')">
|
|
<CheckCircle class="size-4" />
|
|
{{ t('dialog.group.actions.unblock') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-else @click="groupDialogCommand('Block Group')">
|
|
<XCircle class="size-4" />
|
|
{{ t('dialog.group.actions.block') }}
|
|
</DropdownMenuItem>
|
|
</template>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<TabsUnderline
|
|
v-model="groupDialogActiveTab"
|
|
:items="groupDialogTabs"
|
|
:unmount-on-hide="false"
|
|
@update:modelValue="groupDialogTabClick">
|
|
<template #Info>
|
|
<div class="group-banner-image-info">
|
|
<img
|
|
:src="groupDialog.ref.bannerUrl"
|
|
class="cursor-pointer"
|
|
style="flex: none; width: 100%; aspect-ratio: 6/1; object-fit: cover; border-radius: 4px"
|
|
@click="showFullscreenImageDialog(groupDialog.ref.bannerUrl)"
|
|
loading="lazy" />
|
|
</div>
|
|
<div class="x-friend-list" style="max-height: none">
|
|
<span
|
|
v-if="groupDialog.instances.length"
|
|
style="font-size: 12px; font-weight: bold; margin: 5px">
|
|
{{ t('dialog.group.info.instances') }}
|
|
</span>
|
|
<div v-for="room in groupDialog.instances" :key="room.tag" style="width: 100%">
|
|
<div style="margin: 5px 0" class="flex items-center">
|
|
<Location :location="room.tag" />
|
|
<InstanceActionBar
|
|
class="ml-1"
|
|
:location="room.tag"
|
|
:currentlocation="lastLocation.location"
|
|
:instance="room.ref"
|
|
:friendcount="room.friendCount"
|
|
:show-launch="false"
|
|
refresh-tooltip="RefreshCw player count"
|
|
:on-refresh="() => refreshInstancePlayerCount(room.tag)" />
|
|
</div>
|
|
<div
|
|
v-if="room.users.length"
|
|
class="x-friend-list"
|
|
style="margin: 10px 0; padding: 0; max-height: unset">
|
|
<div
|
|
v-for="user in room.users"
|
|
:key="user.id"
|
|
class="x-friend-item x-friend-item-border"
|
|
@click="showUserDialog(user.id)">
|
|
<div class="avatar" :class="userStatusClass(user)">
|
|
<img :src="userImage(user)" loading="lazy" />
|
|
</div>
|
|
<div class="detail">
|
|
<span
|
|
class="name"
|
|
:style="{ color: user.$userColour }"
|
|
v-text="user.displayName" />
|
|
<span v-if="user.location === 'traveling'" class="extra">
|
|
<Spinner class="inline-block mr-1" />
|
|
<Timer :epoch="user.$travelingToTime" />
|
|
</span>
|
|
<span v-else class="extra">
|
|
<Timer :epoch="user.$location_at" />
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="x-friend-item" style="width: 100%; cursor: default">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.announcement') }}</span>
|
|
<span style="display: block" v-text="groupDialog.announcement.title" />
|
|
<div
|
|
v-if="groupDialog.announcement.imageUrl"
|
|
style="display: inline-block; margin-right: 5px">
|
|
<img
|
|
:src="groupDialog.announcement.imageUrl"
|
|
class="cursor-pointer"
|
|
style="
|
|
flex: none;
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 4px;
|
|
object-fit: cover;
|
|
"
|
|
@click="showFullscreenImageDialog(groupDialog.announcement.imageUrl)"
|
|
loading="lazy" />
|
|
</div>
|
|
<pre
|
|
class="extra"
|
|
style="
|
|
display: inline-block;
|
|
vertical-align: top;
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
margin: 0;
|
|
"
|
|
>{{ groupDialog.announcement.text || '-' }}</pre
|
|
>
|
|
<br />
|
|
<div
|
|
v-if="groupDialog.announcement.id"
|
|
class="extra"
|
|
style="float: right; margin-left: 5px">
|
|
<TooltipWrapper v-if="groupDialog.announcement.roleIds.length" side="top">
|
|
<template #content>
|
|
<span>{{ t('dialog.group.posts.visibility') }}</span>
|
|
<br />
|
|
<template v-for="roleId in groupDialog.announcement.roleIds" :key="roleId">
|
|
<template v-for="role in groupDialog.ref.roles" :key="roleId + role.id"
|
|
><span v-if="role.id === roleId" v-text="role.name"
|
|
/></template>
|
|
<span
|
|
v-if="
|
|
groupDialog.announcement.roleIds.indexOf(roleId) <
|
|
groupDialog.announcement.roleIds.length - 1
|
|
">
|
|
,
|
|
</span>
|
|
</template>
|
|
</template>
|
|
<Eye style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
<DisplayName
|
|
:userid="groupDialog.announcement.authorId"
|
|
style="margin-right: 5px" />
|
|
<span v-if="groupDialog.announcement.editorId" style="margin-right: 5px">
|
|
({{ t('dialog.group.posts.edited_by') }}
|
|
<DisplayName :userid="groupDialog.announcement.editorId" />)
|
|
</span>
|
|
<TooltipWrapper side="bottom">
|
|
<template #content>
|
|
<span
|
|
>{{ t('dialog.group.posts.created_at') }}
|
|
{{ formatDateFilter(groupDialog.announcement.createdAt, 'long') }}</span
|
|
>
|
|
<template
|
|
v-if="
|
|
groupDialog.announcement.updatedAt !==
|
|
groupDialog.announcement.createdAt
|
|
">
|
|
<br />
|
|
<span
|
|
>{{ t('dialog.group.posts.edited_at') }}
|
|
{{
|
|
formatDateFilter(groupDialog.announcement.updatedAt, 'long')
|
|
}}</span
|
|
>
|
|
</template>
|
|
</template>
|
|
<Timer :epoch="Date.parse(groupDialog.announcement.updatedAt)" />
|
|
</TooltipWrapper>
|
|
<template v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')">
|
|
<TooltipWrapper side="top" :content="t('dialog.group.posts.edit_tooltip')">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
style="margin-left: 5px; padding: 0"
|
|
@click="
|
|
showGroupPostEditDialog(groupDialog.id, groupDialog.announcement)
|
|
"></Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper side="top" :content="t('dialog.group.posts.delete_tooltip')">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
style="margin-left: 5px; padding: 0"
|
|
@click="confirmDeleteGroupPost(groupDialog.announcement)"></Button>
|
|
</TooltipWrapper>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="x-friend-item" style="width: 100%; cursor: default">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.rules') }}</span>
|
|
<pre
|
|
class="extra"
|
|
style="
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
margin: 0 0.5em 0 0;
|
|
"
|
|
>{{ groupDialog.ref.rules || '-' }}</pre
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="x-friend-item x-friend-item-no-hover" style="width: 100%; cursor: default">
|
|
<div class="detail" style="overflow: visible">
|
|
<span class="name">{{ t('dialog.group.info.upcoming_events') }}</span>
|
|
<template v-if="upcomingCalenderEvents.length > 0">
|
|
<br />
|
|
<div class="grid-view events-row">
|
|
<GroupCalendarEventCard
|
|
v-for="value in upcomingCalenderEvents"
|
|
:key="value.id"
|
|
:event="value"
|
|
:is-following="value.userInterest?.isFollowing"
|
|
@update-following-calendar-data="updateFollowingCalendarData"
|
|
mode="grid"
|
|
card-class="group-dialog-grid-card" />
|
|
</div>
|
|
</template>
|
|
<span v-else class="extra">-</span>
|
|
</div>
|
|
</div>
|
|
<div class="x-friend-item x-friend-item-no-hover" style="width: 100%; cursor: default">
|
|
<div class="detail" style="overflow: visible">
|
|
<span class="name">{{ t('dialog.group.info.past_events') }}</span>
|
|
<template v-if="pastCalenderEvents.length > 0">
|
|
<br />
|
|
<div class="grid-view events-row">
|
|
<GroupCalendarEventCard
|
|
v-for="value in pastCalenderEvents"
|
|
:key="value.id"
|
|
:event="value"
|
|
:is-following="value.userInterest?.isFollowing"
|
|
@update-following-calendar-data="updateFollowingCalendarData"
|
|
mode="grid"
|
|
card-class="group-dialog-grid-card" />
|
|
</div>
|
|
</template>
|
|
<span v-else class="extra">-</span>
|
|
</div>
|
|
</div>
|
|
<div class="x-friend-item" style="cursor: default">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.members') }}</span>
|
|
<div class="extra">
|
|
{{ groupDialog.ref.memberCount }} ({{ groupDialog.ref.onlineMemberCount }})
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="x-friend-item" style="cursor: default">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.created_at') }}</span>
|
|
<span class="extra">{{ formatDateFilter(groupDialog.ref.createdAt, 'long') }}</span>
|
|
</div>
|
|
</div>
|
|
<TooltipWrapper side="top" :content="t('dialog.user.info.open_previous_instance')">
|
|
<div class="x-friend-item" @click="showPreviousInstancesGroupDialog(groupDialog.ref)">
|
|
<div class="detail">
|
|
<span class="name">
|
|
{{ t('dialog.group.info.last_visited') }}
|
|
</span>
|
|
<span class="extra">{{ formatDateFilter(groupDialog.lastVisit, 'long') }}</span>
|
|
</div>
|
|
</div>
|
|
</TooltipWrapper>
|
|
<div class="x-friend-item" style="cursor: default">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.links') }}</span>
|
|
<div
|
|
v-if="groupDialog.ref.links && groupDialog.ref.links.length > 0"
|
|
style="margin-top: 5px"
|
|
class="flex">
|
|
<template v-for="(link, index) in groupDialog.ref.links" :key="index">
|
|
<TooltipWrapper v-if="link">
|
|
<template #content>
|
|
<span v-text="link" />
|
|
</template>
|
|
<img
|
|
:src="getFaviconUrl(link)"
|
|
style="
|
|
width: 16px;
|
|
height: 16px;
|
|
vertical-align: middle;
|
|
margin-right: 5px;
|
|
cursor: pointer;
|
|
"
|
|
@click.stop="openExternalLink(link)"
|
|
loading="lazy" />
|
|
</TooltipWrapper>
|
|
</template>
|
|
</div>
|
|
<div v-else class="extra">-</div>
|
|
</div>
|
|
</div>
|
|
<div class="inline-flex justify-between w-full">
|
|
<div class="x-friend-item" style="cursor: default; width: 50%">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.url') }}</span>
|
|
<span class="extra"
|
|
>{{ groupDialog.ref.$url }}
|
|
<TooltipWrapper side="top" :content="t('dialog.group.info.url_tooltip')">
|
|
<Button
|
|
class="rounded-full ml-1 text-xs"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="copyToClipboard(groupDialog.ref.$url)"
|
|
><Copy class="h-4 w-4" />
|
|
</Button> </TooltipWrapper
|
|
></span>
|
|
</div>
|
|
</div>
|
|
<div class="x-friend-item w-1/2" style="cursor: default; width: 50%">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.id') }}</span>
|
|
<span class="extra"
|
|
>{{ groupDialog.id }}
|
|
<TooltipWrapper side="top" :content="t('dialog.group.info.id_tooltip')">
|
|
<Button
|
|
class="rounded-full ml-1 text-xs"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="copyToClipboard(groupDialog.id)"
|
|
><Copy class="h-4 w-4" />
|
|
</Button> </TooltipWrapper
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="groupDialog.ref.membershipStatus === 'member'"
|
|
style="width: 100%; margin-top: 10px; border-top: 1px solid #e4e7ed14">
|
|
<div style="width: 100%; display: flex; margin-top: 10px">
|
|
<div class="x-friend-item" style="cursor: default">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.joined_at') }}</span>
|
|
<span class="extra">{{
|
|
formatDateFilter(groupDialog.ref.myMember.joinedAt, 'long')
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
<div class="x-friend-item" style="cursor: default">
|
|
<div class="detail">
|
|
<span class="name">{{ t('dialog.group.info.roles') }}</span>
|
|
<span v-if="groupDialog.memberRoles.length === 0" class="extra"> - </span>
|
|
<span v-else class="extra">
|
|
<template v-for="(role, rIndex) in groupDialog.memberRoles" :key="rIndex">
|
|
<TooltipWrapper side="top">
|
|
<template #content>
|
|
<span>{{ t('dialog.group.info.role') }} {{ role.name }}</span>
|
|
<br />
|
|
<span
|
|
>{{ t('dialog.group.info.role_description') }}
|
|
{{ role.description }}</span
|
|
>
|
|
<br />
|
|
<span v-if="role.updatedAt"
|
|
>{{ t('dialog.group.info.role_updated_at') }}
|
|
{{ formatDateFilter(role.updatedAt, 'long') }}</span
|
|
>
|
|
<span v-else
|
|
>{{ t('dialog.group.info.role_created_at') }}
|
|
{{ formatDateFilter(role.createdAt, 'long') }}</span
|
|
>
|
|
<br />
|
|
<span>{{ t('dialog.group.info.role_permissions') }}</span>
|
|
<br />
|
|
<template
|
|
v-for="(permission, pIndex) in role.permissions"
|
|
:key="pIndex">
|
|
<span>{{ permission }}</span>
|
|
<br />
|
|
</template>
|
|
</template>
|
|
<span
|
|
>{{ role.name
|
|
}}{{
|
|
rIndex < groupDialog.memberRoles.length - 1 ? ', ' : ''
|
|
}}</span
|
|
>
|
|
</TooltipWrapper>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #Posts>
|
|
<template v-if="groupDialog.visible">
|
|
<span style="margin-right: 10px; vertical-align: top"
|
|
>{{ t('dialog.group.posts.posts_count') }} {{ groupDialog.posts.length }}</span
|
|
>
|
|
<InputGroupField
|
|
v-model="groupDialog.postsSearch"
|
|
clearable
|
|
size="sm"
|
|
:placeholder="t('dialog.group.posts.search_placeholder')"
|
|
style="width: 89%; margin-bottom: 10px"
|
|
@input="updateGroupPostSearch" />
|
|
<div class="x-friend-list">
|
|
<div
|
|
v-for="post in groupDialog.postsFiltered"
|
|
:key="post.id"
|
|
class="x-friend-item"
|
|
style="width: 100%; cursor: default">
|
|
<div class="detail">
|
|
<span style="display: block" v-text="post.title" />
|
|
<div v-if="post.imageUrl" style="display: inline-block; margin-right: 5px">
|
|
<img
|
|
:src="post.imageUrl"
|
|
class="cursor-pointer"
|
|
style="
|
|
flex: none;
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 4px;
|
|
object-fit: cover;
|
|
"
|
|
@click="showFullscreenImageDialog(post.imageUrl)"
|
|
loading="lazy" />
|
|
</div>
|
|
<pre
|
|
class="extra"
|
|
style="
|
|
display: inline-block;
|
|
vertical-align: top;
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
margin: 0;
|
|
"
|
|
>{{ post.text || '-' }}</pre
|
|
>
|
|
<br />
|
|
<div v-if="post.authorId" class="extra" style="float: right; margin-left: 5px">
|
|
<TooltipWrapper v-if="post.roleIds.length" side="top">
|
|
<template #content>
|
|
<span>{{ t('dialog.group.posts.visibility') }}</span>
|
|
<br />
|
|
<template v-for="roleId in post.roleIds" :key="roleId">
|
|
<template
|
|
v-for="role in groupDialog.ref.roles"
|
|
:key="role.id + roleId"
|
|
><span v-if="role.id === roleId" v-text="role.name" />
|
|
</template>
|
|
<template
|
|
v-if="post.roleIds.indexOf(roleId) < post.roleIds.length - 1"
|
|
><span>, </span></template
|
|
>
|
|
</template>
|
|
</template>
|
|
<Eye style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
<DisplayName :userid="post.authorId" style="margin-right: 5px" />
|
|
<span v-if="post.editorId" style="margin-right: 5px"
|
|
>({{ t('dialog.group.posts.edited_by') }}
|
|
<DisplayName :userid="post.editorId" />)</span
|
|
>
|
|
<TooltipWrapper side="bottom">
|
|
<template #content>
|
|
<span
|
|
>{{ t('dialog.group.posts.created_at') }}
|
|
{{ formatDateFilter(post.createdAt, 'long') }}</span
|
|
>
|
|
<template v-if="post.updatedAt !== post.createdAt">
|
|
<br />
|
|
<span
|
|
>{{ t('dialog.group.posts.edited_at') }}
|
|
{{ formatDateFilter(post.updatedAt, 'long') }}</span
|
|
>
|
|
</template>
|
|
</template>
|
|
<Timer :epoch="Date.parse(post.updatedAt)" />
|
|
</TooltipWrapper>
|
|
<template
|
|
v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')">
|
|
<TooltipWrapper side="top" :content="t('dialog.group.posts.edit_tooltip')">
|
|
<Button
|
|
size="icon-sm"
|
|
class="h-6 w-6 text-xs text-muted-foreground hover:text-foreground"
|
|
variant="ghost"
|
|
@click="showGroupPostEditDialog(groupDialog.id, post)"
|
|
><Pencil class="h-4 w-4" />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper
|
|
side="top"
|
|
:content="t('dialog.group.posts.delete_tooltip')">
|
|
<Button
|
|
size="icon-sm"
|
|
class="h-6 w-6 text-xs text-muted-foreground hover:text-foreground"
|
|
variant="ghost"
|
|
@click="confirmDeleteGroupPost(post)"
|
|
><Trash2 class="h-4 w-4" />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<template #Members>
|
|
<template v-if="groupDialog.visible">
|
|
<span
|
|
v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')"
|
|
style="font-weight: bold; font-size: 16px"
|
|
>{{ t('dialog.group.members.all_members') }}</span
|
|
>
|
|
<span v-else style="font-weight: bold; font-size: 16px">{{
|
|
t('dialog.group.members.friends_only')
|
|
}}</span>
|
|
<div style="margin-top: 10px">
|
|
<Button
|
|
class="rounded-full h-6 w-6"
|
|
variant="outline"
|
|
size="icon-sm"
|
|
:loading="isGroupMembersLoading"
|
|
circle
|
|
@click="loadAllGroupMembers">
|
|
<Spinner v-if="isGroupMembersLoading" /><RefreshCcw v-else
|
|
/></Button>
|
|
<Button
|
|
class="rounded-full h-6 w-6 ml-2"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
style="margin-left: 5px"
|
|
@click="downloadAndSaveJson(`${groupDialog.id}_members`, groupDialog.members)">
|
|
<Download class="h-4 w-4" />
|
|
</Button>
|
|
<span
|
|
v-if="groupDialog.memberSearch.length"
|
|
style="font-size: 14px; margin-left: 5px; margin-right: 5px"
|
|
>{{ groupDialog.memberSearchResults.length }}/{{ groupDialog.ref.memberCount }}</span
|
|
>
|
|
<span v-else style="font-size: 14px; margin-left: 5px; margin-right: 5px"
|
|
>{{ groupDialog.members.length }}/{{ groupDialog.ref.memberCount }}</span
|
|
>
|
|
<div
|
|
v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')"
|
|
style="float: right"
|
|
class="flex items-center">
|
|
<span style="margin-right: 5px">{{ t('dialog.group.members.sort_by') }}</span>
|
|
<Select
|
|
v-model="groupDialogMemberSortValue"
|
|
:disabled="isGroupMembersLoading || groupDialog.memberSearch.length > 0">
|
|
<SelectTrigger class="h-8 w-45 mr-1">
|
|
<SelectValue :placeholder="t('dialog.group.members.sort_by')" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem
|
|
v-for="item in groupDialogSortingOptions"
|
|
:key="item.value"
|
|
:value="item.value">
|
|
{{ t(item.name) }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<span class="ml-2 mr-1">{{ t('dialog.group.members.filter') }}</span>
|
|
<div style="display: inline-block; width: 220px">
|
|
<VirtualCombobox
|
|
v-model="groupDialogMemberFilterKey"
|
|
:groups="groupDialogMemberFilterGroups"
|
|
:disabled="isGroupMembersLoading || groupDialog.memberSearch.length > 0"
|
|
:placeholder="t('dialog.group.members.filter')"
|
|
:search-placeholder="t('dialog.group.members.search')"
|
|
:clearable="false"
|
|
:close-on-select="true">
|
|
<template #trigger="{ text }">
|
|
<span class="truncate">
|
|
{{ text || t('dialog.group.members.filter') }}
|
|
</span>
|
|
</template>
|
|
</VirtualCombobox>
|
|
</div>
|
|
</div>
|
|
<InputGroupField
|
|
v-model="groupDialog.memberSearch"
|
|
:disabled="!hasGroupPermission(groupDialog.ref, 'group-members-manage')"
|
|
clearable
|
|
size="sm"
|
|
:placeholder="t('dialog.group.members.search')"
|
|
style="margin-top: 10px; margin-bottom: 10px"
|
|
@input="groupMembersSearch" />
|
|
</div>
|
|
<div
|
|
v-if="groupDialog.memberSearch.length"
|
|
class="x-friend-list"
|
|
style="margin-top: 10px; overflow: auto; max-height: 250px; min-width: 130px">
|
|
<div
|
|
v-for="user in groupDialog.memberSearchResults"
|
|
:key="user.id"
|
|
class="x-friend-item x-friend-item-border"
|
|
@click="showUserDialog(user.userId)">
|
|
<div class="avatar">
|
|
<img :src="userImage(user.user)" loading="lazy" />
|
|
</div>
|
|
<div class="detail">
|
|
<span
|
|
class="name"
|
|
:style="{ color: user.user?.$userColour }"
|
|
v-text="user.user?.displayName" />
|
|
<span class="extra">
|
|
<template v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')">
|
|
<TooltipWrapper
|
|
v-if="user.isRepresenting"
|
|
side="top"
|
|
:content="t('dialog.group.members.representing')">
|
|
<Tag style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-if="user.visibility !== 'visible'" side="top">
|
|
<template #content>
|
|
<span
|
|
>{{ t('dialog.group.members.visibility') }}
|
|
{{ user.visibility }}</span
|
|
>
|
|
</template>
|
|
<Eye style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
<TooltipWrapper
|
|
v-if="!user.isSubscribedToAnnouncements"
|
|
side="top"
|
|
:content="t('dialog.group.members.unsubscribed_announcements')">
|
|
<MessageSquare style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-if="user.managerNotes" side="top">
|
|
<template #content>
|
|
<span>{{ t('dialog.group.members.manager_notes') }}</span>
|
|
<br />
|
|
<span>{{ user.managerNotes }}</span>
|
|
</template>
|
|
<Pencil style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
</template>
|
|
<template v-for="roleId in user.roleIds" :key="roleId">
|
|
<template v-for="role in groupDialog.ref.roles" :key="role.id + roleId"
|
|
><span v-if="role.id === roleId" v-text="role.name" /></template
|
|
><template v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1"
|
|
><span>, </span></template
|
|
>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul
|
|
v-else-if="groupDialog.members.length > 0"
|
|
class="infinite-list x-friend-list"
|
|
style="margin-top: 10px; overflow: auto; max-height: 250px; min-width: 130px">
|
|
<li
|
|
v-for="user in groupDialog.members"
|
|
:key="user.id"
|
|
class="infinite-list-item x-friend-item x-friend-item-border"
|
|
@click="showUserDialog(user.userId)">
|
|
<div class="avatar">
|
|
<img :src="userImage(user.user)" loading="lazy" />
|
|
</div>
|
|
<div class="detail">
|
|
<span
|
|
class="name"
|
|
:style="{ color: user.user?.$userColour }"
|
|
v-text="user.user?.displayName" />
|
|
<span class="extra">
|
|
<template v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')">
|
|
<TooltipWrapper
|
|
v-if="user.isRepresenting"
|
|
side="top"
|
|
:content="t('dialog.group.members.representing')">
|
|
<Tag style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-if="user.visibility !== 'visible'" side="top">
|
|
<template #content>
|
|
<span
|
|
>{{ t('dialog.group.members.visibility') }}
|
|
{{ user.visibility }}</span
|
|
>
|
|
</template>
|
|
<Eye style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
<TooltipWrapper
|
|
v-if="!user.isSubscribedToAnnouncements"
|
|
side="top"
|
|
:content="t('dialog.group.members.unsubscribed_announcements')">
|
|
<MessageSquare style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-if="user.managerNotes" side="top">
|
|
<template #content>
|
|
<span>{{ t('dialog.group.members.manager_notes') }}</span>
|
|
<br />
|
|
<span>{{ user.managerNotes }}</span>
|
|
</template>
|
|
<Pencil style="margin-right: 5px" />
|
|
</TooltipWrapper>
|
|
</template>
|
|
<template v-for="roleId in user.roleIds" :key="roleId">
|
|
<template v-for="role in groupDialog.ref.roles" :key="roleId + role.id"
|
|
><span v-if="role.id === roleId" v-text="role.name" /></template
|
|
><template v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1"
|
|
><span> </span></template
|
|
>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
</li>
|
|
<div
|
|
v-if="!isGroupMembersDone"
|
|
class="x-friend-item"
|
|
style="width: 100%; height: 45px; text-align: center"
|
|
@click="loadMoreGroupMembers">
|
|
<div v-if="!isGroupMembersLoading" class="detail">
|
|
<span class="name">{{ t('dialog.group.members.load_more') }}</span>
|
|
</div>
|
|
</div>
|
|
</ul>
|
|
</template>
|
|
</template>
|
|
<template #Photos>
|
|
<Button
|
|
class="rounded-full"
|
|
variant="outline"
|
|
size="icon-sm"
|
|
:disabled="isGroupGalleryLoading"
|
|
@click="getGroupGalleries">
|
|
<Spinner v-if="isGroupGalleryLoading" />
|
|
<RefreshCw v-else />
|
|
</Button>
|
|
<TabsUnderline
|
|
v-model="groupDialogGalleryCurrentName"
|
|
:items="groupGalleryTabs"
|
|
:unmount-on-hide="false"
|
|
class="mt-2.5">
|
|
<template
|
|
v-for="(gallery, index) in groupDialog.ref.galleries"
|
|
:key="`label-${index}`"
|
|
v-slot:[`label-${index}`]>
|
|
<span style="font-weight: bold; font-size: 16px" v-text="gallery.name" />
|
|
<i class="x-status-icon" style="margin-left: 5px" :class="groupGalleryStatus(gallery)" />
|
|
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
|
groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0
|
|
}}</span>
|
|
</template>
|
|
<template
|
|
v-for="(gallery, index) in groupDialog.ref.galleries"
|
|
:key="`content-${index}`"
|
|
v-slot:[String(index)]>
|
|
<span style="color: #c7c7c7; padding: 10px" v-text="gallery.description" />
|
|
<div
|
|
style="
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 10px;
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
">
|
|
<Card
|
|
v-for="image in groupDialog.galleries[gallery.id]"
|
|
:key="image.id"
|
|
class="p-0 overflow-hidden transition-shadow hover:shadow-md">
|
|
<img
|
|
:src="image.imageUrl"
|
|
:class="[' cursor-pointer', 'max-w-full', 'max-h-full']"
|
|
@click="showFullscreenImageDialog(image.imageUrl)"
|
|
loading="lazy" />
|
|
</Card>
|
|
</div>
|
|
</template>
|
|
</TabsUnderline>
|
|
</template>
|
|
<template #JSON>
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="refreshGroupDialogTreeData()">
|
|
<RefreshCw />
|
|
</Button>
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="downloadAndSaveJson(groupDialog.id, groupDialog.ref)">
|
|
<Download />
|
|
</Button>
|
|
<vue-json-pretty
|
|
:data="groupDialog.treeData"
|
|
:deep="2"
|
|
:theme="isDarkMode ? 'dark' : 'light'"
|
|
show-icon />
|
|
</template>
|
|
</TabsUnderline>
|
|
</div>
|
|
<GroupPostEditDialog :dialog-data="groupPostEditDialog" :selected-gallery-file="selectedGalleryFile" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
Bell,
|
|
BellOff,
|
|
Bookmark,
|
|
BookmarkCheck,
|
|
Check,
|
|
CheckCircle,
|
|
Copy,
|
|
Download,
|
|
Eye,
|
|
MessageSquare,
|
|
MoreHorizontal,
|
|
Pencil,
|
|
RefreshCw,
|
|
Settings,
|
|
Share2,
|
|
Tag,
|
|
Ticket,
|
|
Trash2,
|
|
X,
|
|
XCircle
|
|
} from 'lucide-vue-next';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card } from '@/components/ui/card';
|
|
import { InputGroupField } from '@/components/ui/input-group';
|
|
import { RefreshCcw } from 'lucide-vue-next';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import { TabsUnderline } from '@/components/ui/tabs';
|
|
import { VirtualCombobox } from '@/components/ui/virtual-combobox';
|
|
import { storeToRefs } from 'pinia';
|
|
import { toast } from 'vue-sonner';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
import VueJsonPretty from 'vue-json-pretty';
|
|
|
|
import {
|
|
copyToClipboard,
|
|
debounce,
|
|
downloadAndSaveJson,
|
|
formatDateFilter,
|
|
getFaviconUrl,
|
|
hasGroupModerationPermission,
|
|
hasGroupPermission,
|
|
languageClass,
|
|
openExternalLink,
|
|
refreshInstancePlayerCount,
|
|
removeFromArray,
|
|
userImage,
|
|
userStatusClass
|
|
} from '../../../shared/utils';
|
|
import {
|
|
useAppearanceSettingsStore,
|
|
useGalleryStore,
|
|
useGroupStore,
|
|
useInstanceStore,
|
|
useLocationStore,
|
|
useModalStore,
|
|
useUserStore
|
|
} from '../../../stores';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger
|
|
} from '../../ui/dropdown-menu';
|
|
import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants';
|
|
import { Badge } from '../../ui/badge';
|
|
import { formatJsonVars } from '../../../shared/utils/base/ui';
|
|
import { groupRequest } from '../../../api';
|
|
|
|
import GroupCalendarEventCard from '../../../views/Tools/components/GroupCalendarEventCard.vue';
|
|
import GroupPostEditDialog from './GroupPostEditDialog.vue';
|
|
import InstanceActionBar from '../../InstanceActionBar.vue';
|
|
import * as workerTimers from 'worker-timers';
|
|
|
|
const { t } = useI18n();
|
|
const groupDialogTabs = computed(() => [
|
|
{ value: 'Info', label: t('dialog.group.info.header') },
|
|
{ value: 'Posts', label: t('dialog.group.posts.header') },
|
|
{ value: 'Members', label: t('dialog.group.members.header') },
|
|
{ value: 'Photos', label: t('dialog.group.gallery.header') },
|
|
{ value: 'JSON', label: t('dialog.group.json.header') }
|
|
]);
|
|
const groupGalleryTabs = computed(() =>
|
|
(groupDialog.value?.ref?.galleries || []).map((gallery, index) => ({
|
|
value: String(index),
|
|
label: gallery?.name ?? ''
|
|
}))
|
|
);
|
|
|
|
const modalStore = useModalStore();
|
|
const instanceStore = useInstanceStore();
|
|
|
|
const { showUserDialog } = useUserStore();
|
|
const { currentUser } = storeToRefs(useUserStore());
|
|
const { groupDialog, inviteGroupDialog } = storeToRefs(useGroupStore());
|
|
const {
|
|
getGroupDialogGroup,
|
|
updateGroupPostSearch,
|
|
showGroupDialog,
|
|
leaveGroupPrompt,
|
|
setGroupVisibility,
|
|
setGroupSubscription,
|
|
applyGroupMember,
|
|
handleGroupMember,
|
|
showGroupMemberModerationDialog
|
|
} = useGroupStore();
|
|
|
|
const { lastLocation } = storeToRefs(useLocationStore());
|
|
const { showFullscreenImageDialog } = useGalleryStore();
|
|
|
|
const { isDarkMode } = storeToRefs(useAppearanceSettingsStore());
|
|
|
|
const groupDialogActiveTab = ref('Info');
|
|
const groupDialogLastActiveTab = ref('Info');
|
|
const isGroupMembersDone = ref(false);
|
|
const isGroupMembersLoading = ref(false);
|
|
const groupDialogGalleryCurrentName = ref('0');
|
|
const groupDialogTabCurrentName = ref('0');
|
|
const isGroupGalleryLoading = ref(false);
|
|
|
|
const groupDialogMemberSortValue = computed({
|
|
get() {
|
|
return groupDialog.value?.memberSortOrder?.value ?? '';
|
|
},
|
|
set(value) {
|
|
const option = Object.values(groupDialogSortingOptions).find((item) => item.value === value);
|
|
if (option) {
|
|
setGroupMemberSortOrder(option);
|
|
}
|
|
}
|
|
});
|
|
|
|
const groupDialogMemberFilterKey = computed({
|
|
get() {
|
|
const filter = groupDialog.value?.memberFilter;
|
|
if (!filter) return null;
|
|
|
|
if (filter.id === null) return 'everyone';
|
|
if (filter.id === '') return 'usersWithNoRole';
|
|
return `role:${filter.id}`;
|
|
},
|
|
set(key) {
|
|
if (!key) return;
|
|
|
|
if (key === 'everyone') {
|
|
setGroupMemberFilter(groupDialogFilterOptions.everyone);
|
|
return;
|
|
}
|
|
if (key === 'usersWithNoRole') {
|
|
setGroupMemberFilter(groupDialogFilterOptions.usersWithNoRole);
|
|
return;
|
|
}
|
|
|
|
if (key.startsWith('role:')) {
|
|
const roleId = key.slice('role:'.length);
|
|
const role = groupDialog.value?.ref?.roles?.find((r) => r.id === roleId);
|
|
if (role) {
|
|
setGroupMemberFilter(role);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const groupDialogMemberFilterGroups = computed(() => {
|
|
const filterItems = Object.values(groupDialogFilterOptions).map((item) => ({
|
|
value: item.id === null ? 'everyone' : item.id === '' ? 'usersWithNoRole' : `role:${item.id}`,
|
|
label: t(item.name),
|
|
search: t(item.name)
|
|
}));
|
|
|
|
const roleItems = (groupDialog.value?.ref?.roles ?? [])
|
|
.filter((role) => !role.defaultRole)
|
|
.map((role) => ({
|
|
value: `role:${role.id}`,
|
|
label: role.name,
|
|
search: role.name
|
|
}));
|
|
|
|
return [
|
|
{
|
|
key: 'filters',
|
|
label: t('dialog.group.members.filter'),
|
|
items: filterItems
|
|
},
|
|
{
|
|
key: 'roles',
|
|
label: 'Roles',
|
|
items: roleItems
|
|
}
|
|
].filter((group) => group.items.length);
|
|
});
|
|
const selectedGalleryFile = ref({
|
|
selectedFileId: '',
|
|
selectedImageUrl: ''
|
|
});
|
|
const groupPostEditDialog = reactive({
|
|
visible: false,
|
|
groupRef: {},
|
|
title: '',
|
|
text: '',
|
|
sendNotification: true,
|
|
visibility: 'group',
|
|
roleIds: [],
|
|
postId: '',
|
|
groupId: ''
|
|
});
|
|
|
|
let loadMoreGroupMembersParams = ref({
|
|
n: 100,
|
|
offset: 0,
|
|
groupId: '',
|
|
sort: '',
|
|
roleId: ''
|
|
});
|
|
|
|
const pastCalenderEvents = computed(() => {
|
|
if (!groupDialog.value.calendar) {
|
|
return [];
|
|
}
|
|
const now = Date.now();
|
|
return groupDialog.value.calendar.filter((event) => {
|
|
const eventEnd = new Date(event.endsAt).getTime();
|
|
return eventEnd < now;
|
|
});
|
|
});
|
|
|
|
const upcomingCalenderEvents = computed(() => {
|
|
if (!groupDialog.value.calendar) {
|
|
return [];
|
|
}
|
|
const now = Date.now();
|
|
return groupDialog.value.calendar.filter((event) => {
|
|
const eventEnd = new Date(event.endsAt).getTime();
|
|
return eventEnd >= now;
|
|
});
|
|
});
|
|
|
|
watch(
|
|
() => groupDialog.value.isGetGroupDialogGroupLoading,
|
|
(val) => {
|
|
if (val) {
|
|
loadLastActiveTab();
|
|
}
|
|
}
|
|
);
|
|
|
|
function showInviteGroupDialog(groupId, userId) {
|
|
if (groupId) {
|
|
inviteGroupDialog.value.groupId = groupId;
|
|
}
|
|
if (userId) {
|
|
inviteGroupDialog.value.userId = userId;
|
|
}
|
|
inviteGroupDialog.value.visible = true;
|
|
}
|
|
|
|
function showPreviousInstancesGroupDialog(groupRef) {
|
|
instanceStore.showPreviousInstancesGroupDialog(groupRef);
|
|
}
|
|
|
|
function setGroupRepresentation(groupId) {
|
|
handleGroupRepresentationChange(groupId, true);
|
|
}
|
|
function clearGroupRepresentation(groupId) {
|
|
handleGroupRepresentationChange(groupId, false);
|
|
}
|
|
|
|
function groupMembersSearch() {
|
|
if (groupDialog.value.memberSearch.length < 3) {
|
|
groupDialog.value.memberSearchResults = [];
|
|
isGroupMembersLoading.value = false;
|
|
return;
|
|
}
|
|
debounce(groupMembersSearchDebounced, 200)();
|
|
}
|
|
|
|
function groupMembersSearchDebounced() {
|
|
const D = groupDialog.value;
|
|
const search = D.memberSearch;
|
|
D.memberSearchResults = [];
|
|
if (!search || search.length < 3) {
|
|
return;
|
|
}
|
|
isGroupMembersLoading.value = true;
|
|
groupRequest
|
|
.getGroupMembersSearch({
|
|
groupId: D.id,
|
|
query: search,
|
|
n: 100,
|
|
offset: 0
|
|
})
|
|
.then((args) => {
|
|
for (const json of args.json.results) {
|
|
handleGroupMember({
|
|
json,
|
|
params: {
|
|
groupId: args.params.groupId
|
|
}
|
|
});
|
|
}
|
|
if (D.id === args.params.groupId) {
|
|
D.memberSearchResults = args.json.results;
|
|
}
|
|
})
|
|
.finally(() => {
|
|
isGroupMembersLoading.value = false;
|
|
});
|
|
}
|
|
|
|
function handleGroupRepresentationChange(groupId, isSet) {
|
|
groupRequest
|
|
.setGroupRepresentation(groupId, {
|
|
isRepresenting: isSet
|
|
})
|
|
.then((args) => {
|
|
if (groupDialog.value.visible && groupDialog.value.id === args.groupId) {
|
|
updateGroupDialogData({
|
|
...groupDialog.value,
|
|
ref: { ...groupDialog.value.ref, isRepresenting: args.params.isRepresenting }
|
|
});
|
|
getGroupDialogGroup(groupId);
|
|
}
|
|
});
|
|
}
|
|
|
|
function cancelGroupRequest(id) {
|
|
groupRequest
|
|
.cancelGroupRequest({
|
|
groupId: id
|
|
})
|
|
.then(() => {
|
|
if (groupDialog.value.visible && groupDialog.value.id === id) {
|
|
getGroupDialogGroup(id);
|
|
}
|
|
});
|
|
}
|
|
function confirmDeleteGroupPost(post) {
|
|
modalStore
|
|
.confirm({
|
|
description: 'Are you sure you want to delete this post?',
|
|
title: 'Confirm'
|
|
})
|
|
.then(({ ok }) => {
|
|
if (!ok) return;
|
|
groupRequest
|
|
.deleteGroupPost({
|
|
groupId: post.groupId,
|
|
postId: post.id
|
|
})
|
|
.then((args) => {
|
|
const D = groupDialog.value;
|
|
if (D.id !== args.params.groupId) {
|
|
return;
|
|
}
|
|
|
|
const postId = args.params.postId;
|
|
// remove existing post
|
|
for (const item of D.posts) {
|
|
if (item.id === postId) {
|
|
removeFromArray(D.posts, item);
|
|
break;
|
|
}
|
|
}
|
|
// remove/update announcement
|
|
if (postId === D.announcement.id) {
|
|
if (D.posts.length > 0) {
|
|
D.announcement = D.posts[0];
|
|
} else {
|
|
D.announcement = {};
|
|
}
|
|
}
|
|
updateGroupPostSearch();
|
|
});
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function groupGalleryStatus(gallery) {
|
|
const style = {};
|
|
if (!gallery.membersOnly) {
|
|
style.blue = true;
|
|
} else if (!gallery.roleIdsToView) {
|
|
style.green = true;
|
|
} else {
|
|
style.red = true;
|
|
}
|
|
return style;
|
|
}
|
|
|
|
function groupDialogCommand(command) {
|
|
const D = groupDialog.value;
|
|
if (D.visible === false) {
|
|
return;
|
|
}
|
|
switch (command) {
|
|
case 'Share':
|
|
copyToClipboard(groupDialog.value.ref.$url);
|
|
break;
|
|
case 'Create Post':
|
|
showGroupPostEditDialog(groupDialog.value.id, null);
|
|
break;
|
|
case 'Moderation Tools':
|
|
showGroupMemberModerationDialog(groupDialog.value.id);
|
|
break;
|
|
case 'Invite To Group':
|
|
showInviteGroupDialog(D.id, '');
|
|
break;
|
|
case 'Refresh':
|
|
showGroupDialog(D.id);
|
|
break;
|
|
case 'Leave Group':
|
|
leaveGroupPrompt(D.id);
|
|
break;
|
|
case 'Block Group':
|
|
blockGroup(D.id);
|
|
break;
|
|
case 'Unblock Group':
|
|
unblockGroup(D.id);
|
|
break;
|
|
case 'Visibility Everyone':
|
|
setGroupVisibility(D.id, 'visible');
|
|
break;
|
|
case 'Visibility Friends':
|
|
setGroupVisibility(D.id, 'friends');
|
|
break;
|
|
case 'Visibility Hidden':
|
|
setGroupVisibility(D.id, 'hidden');
|
|
break;
|
|
case 'Subscribe To Announcements':
|
|
setGroupSubscription(D.id, true);
|
|
break;
|
|
case 'Unsubscribe To Announcements':
|
|
setGroupSubscription(D.id, false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function blockGroup(groupId) {
|
|
modalStore
|
|
.confirm({
|
|
description: 'Are you sure you want to block this group?',
|
|
title: 'Confirm'
|
|
})
|
|
.then(({ ok }) => {
|
|
if (!ok) return;
|
|
groupRequest
|
|
.blockGroup({
|
|
groupId
|
|
})
|
|
.then((args) => {
|
|
if (groupDialog.value.visible && groupDialog.value.id === args.params.groupId) {
|
|
showGroupDialog(args.params.groupId);
|
|
}
|
|
});
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function unblockGroup(groupId) {
|
|
modalStore
|
|
.confirm({
|
|
description: 'Are you sure you want to unblock this group?',
|
|
title: 'Confirm'
|
|
})
|
|
.then(({ ok }) => {
|
|
if (!ok) return;
|
|
groupRequest
|
|
.unblockGroup({
|
|
groupId,
|
|
userId: currentUser.value.id
|
|
})
|
|
.then((args) => {
|
|
if (groupDialog.value.visible && groupDialog.value.id === args.params.groupId) {
|
|
showGroupDialog(args.params.groupId);
|
|
}
|
|
});
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function joinGroup(id) {
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
return groupRequest
|
|
.joinGroup({
|
|
groupId: id
|
|
})
|
|
.then((args) => {
|
|
if (groupDialog.value.visible && groupDialog.value.id === id) {
|
|
updateGroupDialogData({
|
|
...groupDialog.value,
|
|
inGroup: args.json.membershipStatus === 'member'
|
|
});
|
|
// groupDialog.value.inGroup = json.membershipStatus === 'member';
|
|
getGroupDialogGroup(id);
|
|
}
|
|
if (args.json.membershipStatus === 'member') {
|
|
toast.success('Group joined');
|
|
} else if (args.json.membershipStatus === 'requested') {
|
|
toast.success('Group join request sent');
|
|
}
|
|
return args;
|
|
});
|
|
}
|
|
|
|
function handleGroupDialogTab(tabName) {
|
|
groupDialogLastActiveTab.value = tabName;
|
|
if (tabName === 'Members') {
|
|
getGroupDialogGroupMembers();
|
|
} else if (tabName === 'Photos') {
|
|
getGroupGalleries();
|
|
} else if (tabName === 'JSON') {
|
|
refreshGroupDialogTreeData();
|
|
}
|
|
}
|
|
|
|
function loadLastActiveTab() {
|
|
handleGroupDialogTab(groupDialogLastActiveTab.value);
|
|
}
|
|
|
|
function groupDialogTabClick(tabName) {
|
|
if (tabName === groupDialogTabCurrentName.value) {
|
|
if (tabName === 'JSON') {
|
|
refreshGroupDialogTreeData();
|
|
}
|
|
return;
|
|
}
|
|
handleGroupDialogTab(tabName);
|
|
groupDialogTabCurrentName.value = tabName;
|
|
}
|
|
|
|
function showGroupPostEditDialog(groupId, post) {
|
|
const D = groupPostEditDialog;
|
|
D.sendNotification = true;
|
|
D.groupRef = {};
|
|
D.title = '';
|
|
D.text = '';
|
|
D.visibility = 'group';
|
|
D.roleIds = [];
|
|
D.postId = '';
|
|
D.groupId = groupId;
|
|
selectedGalleryFile.value = {
|
|
selectedFileId: '',
|
|
selectedImageUrl: ''
|
|
};
|
|
|
|
if (post) {
|
|
D.title = post.title;
|
|
D.text = post.text;
|
|
D.visibility = post.visibility;
|
|
D.roleIds = post.roleIds;
|
|
D.postId = post.id;
|
|
selectedGalleryFile.value = {
|
|
selectedFileId: post.imageId,
|
|
selectedImageUrl: post.imageUrl
|
|
};
|
|
}
|
|
groupRequest.getCachedGroup({ groupId }).then((args) => {
|
|
D.groupRef = args.ref;
|
|
});
|
|
D.visible = true;
|
|
}
|
|
|
|
async function getGroupDialogGroupMembers() {
|
|
const D = groupDialog.value;
|
|
D.members = [];
|
|
isGroupMembersDone.value = false;
|
|
loadMoreGroupMembersParams.value = {
|
|
sort: 'joinedAt:desc',
|
|
roleId: '',
|
|
n: 100,
|
|
offset: 0,
|
|
groupId: D.id
|
|
};
|
|
if (D.memberSortOrder.value) {
|
|
loadMoreGroupMembersParams.value.sort = D.memberSortOrder.value;
|
|
}
|
|
if (D.memberFilter.id !== null) {
|
|
loadMoreGroupMembersParams.value.roleId = D.memberFilter.id;
|
|
}
|
|
if (D.inGroup) {
|
|
await groupRequest
|
|
.getGroupMember({
|
|
groupId: D.id,
|
|
userId: currentUser.value.id
|
|
})
|
|
.then((args) => {
|
|
args.ref = applyGroupMember(args.json);
|
|
if (args.json) {
|
|
args.json.user = currentUser.value;
|
|
if (D.memberFilter.id === null) {
|
|
// when flitered by role don't include self
|
|
D.members.push(args.json);
|
|
}
|
|
}
|
|
return args;
|
|
});
|
|
}
|
|
await loadMoreGroupMembers();
|
|
}
|
|
|
|
async function loadMoreGroupMembers() {
|
|
if (isGroupMembersDone.value || isGroupMembersLoading.value) {
|
|
return;
|
|
}
|
|
const D = groupDialog.value;
|
|
const params = loadMoreGroupMembersParams.value;
|
|
if (params.roleId === '') {
|
|
delete params.roleId;
|
|
}
|
|
D.memberSearch = '';
|
|
isGroupMembersLoading.value = true;
|
|
await groupRequest
|
|
.getGroupMembers(params)
|
|
.finally(() => {
|
|
isGroupMembersLoading.value = false;
|
|
})
|
|
.then((args) => {
|
|
for (const json of args.json) {
|
|
handleGroupMember({
|
|
json,
|
|
params: {
|
|
groupId: args.params.groupId
|
|
}
|
|
});
|
|
}
|
|
for (let i = 0; i < args.json.length; i++) {
|
|
const member = args.json[i];
|
|
if (member.userId === currentUser.value.id) {
|
|
if (D.members.length > 0 && D.members[0].userId === currentUser.value.id) {
|
|
// remove duplicate and keep sort order
|
|
D.members.splice(0, 1);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (args.json.length < params.n) {
|
|
isGroupMembersDone.value = true;
|
|
}
|
|
D.members = [...D.members, ...args.json];
|
|
params.offset += params.n;
|
|
return args;
|
|
})
|
|
.catch((err) => {
|
|
isGroupMembersDone.value = true;
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
async function getGroupGalleries() {
|
|
updateGroupDialogData({ ...groupDialog.value, galleries: {} });
|
|
groupDialogGalleryCurrentName.value = '0';
|
|
isGroupGalleryLoading.value = true;
|
|
for (let i = 0; i < groupDialog.value.ref.galleries.length; i++) {
|
|
const gallery = groupDialog.value.ref.galleries[i];
|
|
await getGroupGallery(groupDialog.value.id, gallery.id);
|
|
}
|
|
isGroupGalleryLoading.value = false;
|
|
}
|
|
|
|
async function getGroupGallery(groupId, galleryId) {
|
|
try {
|
|
const params = {
|
|
groupId,
|
|
galleryId,
|
|
n: 100,
|
|
offset: 0
|
|
};
|
|
const count = 50; // 5000 max
|
|
for (let i = 0; i < count; i++) {
|
|
const args = await groupRequest.getGroupGallery(params);
|
|
if (args) {
|
|
for (const json of args.json) {
|
|
if (groupDialog.value.id === json.groupId) {
|
|
if (!groupDialog.value.galleries[json.galleryId]) {
|
|
groupDialog.value.galleries[json.galleryId] = [];
|
|
}
|
|
groupDialog.value.galleries[json.galleryId].push(json);
|
|
}
|
|
}
|
|
}
|
|
params.offset += 100;
|
|
if (args.json.length < 100) {
|
|
break;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
function refreshGroupDialogTreeData() {
|
|
const D = groupDialog.value;
|
|
const treeData = {
|
|
group: formatJsonVars(D.ref),
|
|
posts: D.posts,
|
|
instances: D.instances,
|
|
members: D.members,
|
|
galleries: D.galleries
|
|
};
|
|
updateGroupDialogData({
|
|
...groupDialog.value,
|
|
treeData
|
|
});
|
|
}
|
|
|
|
async function loadAllGroupMembers() {
|
|
if (isGroupMembersLoading.value) {
|
|
return;
|
|
}
|
|
await getGroupDialogGroupMembers();
|
|
while (groupDialog.value.visible && !isGroupMembersDone.value) {
|
|
isGroupMembersLoading.value = true;
|
|
await new Promise((resolve) => {
|
|
workerTimers.setTimeout(resolve, 1000);
|
|
});
|
|
isGroupMembersLoading.value = false;
|
|
await loadMoreGroupMembers();
|
|
}
|
|
}
|
|
|
|
async function setGroupMemberSortOrder(sortOrder) {
|
|
const D = groupDialog.value;
|
|
if (D.memberSortOrder.value === sortOrder) {
|
|
return;
|
|
}
|
|
D.memberSortOrder = sortOrder;
|
|
await getGroupDialogGroupMembers();
|
|
}
|
|
|
|
async function setGroupMemberFilter(filter) {
|
|
const D = groupDialog.value;
|
|
if (D.memberFilter === filter) {
|
|
return;
|
|
}
|
|
D.memberFilter = filter;
|
|
await getGroupDialogGroupMembers();
|
|
}
|
|
|
|
function updateGroupDialogData(obj) {
|
|
groupDialog.value = {
|
|
...groupDialog.value,
|
|
...obj
|
|
};
|
|
}
|
|
|
|
function updateFollowingCalendarData(event) {
|
|
const calendar = groupDialog.value.calendar;
|
|
for (let i = 0; i < calendar.length; i++) {
|
|
if (calendar[i].id === event.id) {
|
|
calendar[i] = {
|
|
...calendar[i],
|
|
...event
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style scoped>
|
|
.time-group-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
overflow: visible;
|
|
}
|
|
.events-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
overflow-y: auto;
|
|
max-height: 360px;
|
|
padding: 9px 0;
|
|
}
|
|
</style>
|