mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
752 lines
28 KiB
TypeScript
752 lines
28 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {Routes} from '@app/app/Routes';
|
|
import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
|
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
|
import styles from '@app/features/channel/components/InviteEmbed.module.css';
|
|
import {
|
|
getGroupDMTitle,
|
|
getGuildEmbedSplashAspectRatio,
|
|
getImageAspectRatioFromBase64,
|
|
} from '@app/features/channel/components/invite_embed/InviteEmbedUtils';
|
|
import {useMaybeMessageViewContext} from '@app/features/channel/components/MessageViewContext';
|
|
import type {Channel} from '@app/features/channel/models/Channel';
|
|
import {clampWideAssetAspectRatio} from '@app/features/expressions/utils/AssetImageGeometry';
|
|
import {GuildBadge} from '@app/features/guild/components/GuildBadge';
|
|
import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon';
|
|
import GuildCount from '@app/features/guild/state/GuildCount';
|
|
import Guilds from '@app/features/guild/state/Guilds';
|
|
import {
|
|
JOIN_COMMUNITY_DESCRIPTOR,
|
|
NO_DESCRIPTION_PROVIDED_DESCRIPTOR,
|
|
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
|
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
|
import * as InviteCommands from '@app/features/invite/commands/InviteCommands';
|
|
import Invites from '@app/features/invite/state/Invites';
|
|
import {
|
|
isGroupDmInvite,
|
|
isGuildInvite,
|
|
isPackInvite as isPackInviteGuard,
|
|
} from '@app/features/invite/types/InviteTypes';
|
|
import {getGroupDmInviteCounts} from '@app/features/invite/utils/GroupDmInviteCounts';
|
|
import {
|
|
GuildInvitePrimaryAction,
|
|
getGuildInviteActionState,
|
|
getGuildInvitePrimaryAction,
|
|
isGuildInviteActionDisabled,
|
|
} from '@app/features/invite/utils/GuildInviteActionState';
|
|
import {
|
|
EmbedCard,
|
|
EmbedSkeletonButton,
|
|
EmbedSkeletonCircle,
|
|
EmbedSkeletonDot,
|
|
EmbedSkeletonIcon,
|
|
EmbedSkeletonStatLong,
|
|
EmbedSkeletonStatShort,
|
|
EmbedSkeletonTitle,
|
|
} from '@app/features/messaging/components/embeds/embed_card/EmbedCard';
|
|
import cardStyles from '@app/features/messaging/components/embeds/embed_card/EmbedCard.module.css';
|
|
import {useEmbedSkeletonOverride} from '@app/features/messaging/components/embeds/embed_card/useEmbedSkeletonOverride';
|
|
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
|
import * as RouterUtils from '@app/features/navigation/utils/RouterUtils';
|
|
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
|
|
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
|
|
import {InviteEmbedContextMenu} from '@app/features/ui/action_menu/InviteEmbedContextMenu';
|
|
import {Button} from '@app/features/ui/button/Button';
|
|
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
|
|
import {Avatar} from '@app/features/ui/components/Avatar';
|
|
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
|
import {User} from '@app/features/user/models/User';
|
|
import Users from '@app/features/user/state/Users';
|
|
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
|
import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils';
|
|
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
|
|
import {msg} from '@lingui/core/macro';
|
|
import {Plural, Trans, useLingui} from '@lingui/react/macro';
|
|
import {QuestionIcon} from '@phosphor-icons/react';
|
|
import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting';
|
|
import {observer} from 'mobx-react-lite';
|
|
import type React from 'react';
|
|
import {useEffect, useMemo, useRef, useState} from 'react';
|
|
|
|
const UNNAMED_GROUP_DESCRIPTOR = msg({
|
|
message: 'Unnamed group',
|
|
comment: 'Fallback title in an invite embed for a group DM with no name set.',
|
|
});
|
|
const ALREADY_JOINED_DESCRIPTOR = msg({
|
|
message: 'Already joined',
|
|
comment: 'Status label on a group DM invite embed when the current user is already a member.',
|
|
});
|
|
const JOIN_GROUP_DESCRIPTOR = msg({
|
|
message: 'Join group',
|
|
comment: 'Button label on a group DM invite embed that accepts the invite.',
|
|
});
|
|
const EMOJI_PACK_DESCRIPTOR = msg({
|
|
message: 'Emoji pack',
|
|
comment: 'Kind label on an emoji pack invite embed.',
|
|
});
|
|
const STICKER_PACK_DESCRIPTOR = msg({
|
|
message: 'Sticker pack',
|
|
comment: 'Kind label on a sticker pack invite embed.',
|
|
});
|
|
const INSTALL_EMOJI_PACK_DESCRIPTOR = msg({
|
|
message: 'Install emoji pack',
|
|
comment: 'Button label on an emoji pack invite embed that installs the pack.',
|
|
});
|
|
const INSTALL_STICKER_PACK_DESCRIPTOR = msg({
|
|
message: 'Install sticker pack',
|
|
comment: 'Button label on a sticker pack invite embed that installs the pack.',
|
|
});
|
|
const ACCEPTING_THIS_INVITE_INSTALLS_THE_PACK_AUTOMATICALLY_DESCRIPTOR = msg({
|
|
message: 'Accepting this invite installs the pack automatically.',
|
|
comment: 'Helper text on a pack invite embed explaining that accepting installs the pack.',
|
|
});
|
|
const INVITES_DISABLED_DESCRIPTOR = msg({
|
|
message: 'Invites disabled',
|
|
comment: 'Status label on a community invite embed when invites are paused or anti-raid mode is active.',
|
|
});
|
|
const GO_TO_COMMUNITY_DESCRIPTOR = msg({
|
|
message: 'Go to community',
|
|
comment: 'Button label on a community invite embed when the current user is already a member.',
|
|
});
|
|
const ONLINE_DESCRIPTOR = msg({
|
|
message: '{renderedPresenceCount} online',
|
|
comment:
|
|
'Presence summary on a community invite embed. renderedPresenceCount is a formatted count of online members.',
|
|
});
|
|
const DETECTED_A_POTENTIAL_RAID_SO_NEW_USERS_CAN_DESCRIPTOR = msg({
|
|
message: "{productName} detected a potential raid, so new users can't join right now.",
|
|
comment: 'Anti-raid status message on a community invite embed. productName is the product brand name placeholder.',
|
|
});
|
|
const INVITES_ARE_CURRENTLY_PAUSED_FOR_THIS_COMMUNITY_DESCRIPTOR = msg({
|
|
message: 'Invites are paused for this community.',
|
|
comment: 'Status message on a community invite embed when invites are paused by community admins.',
|
|
});
|
|
const UNKNOWN_INVITE_DESCRIPTOR = msg({
|
|
message: 'Unknown invite',
|
|
comment: 'Title shown on an invite embed when the invite code resolves to nothing (expired, revoked, or invalid).',
|
|
});
|
|
const TRY_ASKING_FOR_A_NEW_INVITE_DESCRIPTOR = msg({
|
|
message: 'Try asking for a new invite.',
|
|
comment: 'Helper text on an unknown invite embed suggesting the user request a fresh invite.',
|
|
});
|
|
const INVITE_UNAVAILABLE_DESCRIPTOR = msg({
|
|
message: 'Invite unavailable',
|
|
comment: 'Title shown on an invite embed when the invite cannot be resolved due to a non-permanent failure.',
|
|
});
|
|
const INVITE_MASKED_DESCRIPTOR = msg({
|
|
message: 'Invite masked',
|
|
comment: 'Title shown on an invite embed while streaming privacy is active.',
|
|
});
|
|
const THIS_INVITE_IS_MASKED_WHILE_SHARING_DESCRIPTOR = msg({
|
|
message: 'This invite is hidden while sharing is protected.',
|
|
comment: 'Helper text shown on an invite embed while streaming privacy is active.',
|
|
});
|
|
const createTitleKeyDownHandler = (callback: () => void) => (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
if (isKeyboardActivationKey(event.key)) {
|
|
event.preventDefault();
|
|
callback();
|
|
}
|
|
};
|
|
|
|
function formatInviteCount(value: number): string {
|
|
return formatNumber(value, getCurrentLocale());
|
|
}
|
|
|
|
interface InviteEmbedProps {
|
|
code: string;
|
|
message?: Message;
|
|
sourceChannel?: Channel | null;
|
|
onDelete?: (bypassConfirm?: boolean) => void;
|
|
}
|
|
|
|
export const InviteEmbed = observer(function InviteEmbed({code, message, sourceChannel, onDelete}: InviteEmbedProps) {
|
|
if (StreamerMode.shouldHideInviteLinks) {
|
|
return <InviteHiddenState data-flx="channel.invite-embed.invite-hidden-state" />;
|
|
}
|
|
return (
|
|
<InviteEmbedInner
|
|
code={code}
|
|
message={message}
|
|
sourceChannel={sourceChannel}
|
|
onDelete={onDelete}
|
|
data-flx="channel.invite-embed.inner"
|
|
/>
|
|
);
|
|
});
|
|
|
|
const InviteEmbedInner = observer(function InviteEmbedInner({
|
|
code,
|
|
message,
|
|
sourceChannel,
|
|
onDelete,
|
|
}: InviteEmbedProps) {
|
|
const {i18n} = useLingui();
|
|
const inviteState = Invites.invites.get(code) ?? null;
|
|
const shouldForceSkeleton = useEmbedSkeletonOverride();
|
|
const invite = inviteState?.data ?? null;
|
|
const isPackInvite = invite != null && isPackInviteGuard(invite);
|
|
const isGuildInviteType = invite != null && isGuildInvite(invite);
|
|
const packCreatorRecord = useMemo(() => {
|
|
if (!isPackInvite || !invite) return null;
|
|
return new User(invite.pack.creator);
|
|
}, [invite, isPackInvite]);
|
|
const guildFromInvite = isGuildInviteType ? invite!.guild : null;
|
|
const guild = Guilds.getGuild(guildFromInvite?.id ?? '') || guildFromInvite;
|
|
const embedSplash = guild != null ? ('embedSplash' in guild ? guild.embedSplash : guild.embed_splash) : undefined;
|
|
const splashURL =
|
|
guild != null ? AvatarUtils.getGuildEmbedSplashURL({id: guild.id, embedSplash: embedSplash || null}) : null;
|
|
const messageViewContext = useMaybeMessageViewContext();
|
|
const currentChannelId = messageViewContext?.channel.id;
|
|
const inviteWrapperRef = useRef<HTMLDivElement | null>(null);
|
|
const isLoading = shouldForceSkeleton || !inviteState || inviteState.loading;
|
|
const prevLoadingRef = useRef(true);
|
|
const prevCodeRef = useRef(code);
|
|
useEffect(() => {
|
|
if (prevCodeRef.current !== code) {
|
|
prevLoadingRef.current = true;
|
|
prevCodeRef.current = code;
|
|
}
|
|
}, [code]);
|
|
useEffect(() => {
|
|
if (prevLoadingRef.current && !isLoading && currentChannelId) {
|
|
ComponentDispatch.dispatch('LAYOUT_RESIZED', {channelId: currentChannelId});
|
|
}
|
|
prevLoadingRef.current = isLoading;
|
|
}, [isLoading, currentChannelId]);
|
|
useEffect(() => {
|
|
if (!inviteState) {
|
|
void InviteCommands.fetchWithCoalescing(code).catch(() => {});
|
|
}
|
|
}, [code, inviteState]);
|
|
const memberGuildId = isGuildInviteType && Guilds.getGuild(guildFromInvite?.id ?? '') ? guild!.id : null;
|
|
useEffect(() => {
|
|
if (memberGuildId) {
|
|
GuildCount.requestCounts(memberGuildId);
|
|
}
|
|
}, [memberGuildId]);
|
|
let content: React.ReactNode;
|
|
if (shouldForceSkeleton || !inviteState || inviteState.loading) {
|
|
content = <InviteLoadingState data-flx="channel.invite-embed.invite-loading-state" />;
|
|
} else if (inviteState.error || !invite) {
|
|
content = <InviteNotFoundError data-flx="channel.invite-embed.invite-not-found-error" />;
|
|
} else if (isGroupDmInvite(invite)) {
|
|
const inviter = Users.getUser(invite.inviter?.id ?? '');
|
|
const groupDMTitle = getGroupDMTitle(invite.channel, i18n._(UNNAMED_GROUP_DESCRIPTOR));
|
|
const groupDMPath = Routes.dmChannel(invite.channel.id);
|
|
const handleAcceptInvite = () => InviteCommands.acceptAndTransitionToChannel(invite.code, i18n);
|
|
const handleNavigateToGroup = () => RouterUtils.transitionTo(groupDMPath);
|
|
const groupDMCounts = getGroupDmInviteCounts({
|
|
channelId: invite.channel.id,
|
|
inviteMemberCount: invite.member_count,
|
|
});
|
|
const isAlreadyInGroupDM = groupDMCounts.hasLocalChannel;
|
|
const memberCount = groupDMCounts.memberCount;
|
|
const renderedMemberCount = formatInviteCount(memberCount);
|
|
content = (
|
|
<EmbedCard
|
|
splashURL={null}
|
|
headerClassName={styles.headerInvite}
|
|
icon={
|
|
inviter ? (
|
|
<Avatar user={inviter} size={48} className={styles.icon} data-flx="channel.invite-embed.icon" />
|
|
) : (
|
|
<div className={styles.iconFallback} data-flx="channel.invite-embed.icon-fallback" />
|
|
)
|
|
}
|
|
title={
|
|
<div className={styles.titleContainer} data-flx="channel.invite-embed.title-container">
|
|
<h3
|
|
className={`${cardStyles.title} ${cardStyles.titlePrimary} ${styles.titleText}`}
|
|
data-flx="channel.invite-embed.title-text"
|
|
>
|
|
<FocusRing offset={-2} data-flx="channel.invite-embed.focus-ring">
|
|
<button
|
|
type="button"
|
|
className={cardStyles.titleButton}
|
|
onClick={handleNavigateToGroup}
|
|
onKeyDown={createTitleKeyDownHandler(handleNavigateToGroup)}
|
|
data-flx="channel.invite-embed.button.navigate-to-group"
|
|
>
|
|
{groupDMTitle}
|
|
</button>
|
|
</FocusRing>
|
|
</h3>
|
|
</div>
|
|
}
|
|
body={
|
|
<div className={styles.stats} data-flx="channel.invite-embed.stats">
|
|
<div className={styles.stat} data-flx="channel.invite-embed.stat">
|
|
<div className={`${styles.statDot} ${styles.statDotMembers}`} data-flx="channel.invite-embed.stat-dot" />
|
|
<span className={styles.statText} data-flx="channel.invite-embed.stat-text">
|
|
<Trans>
|
|
{renderedMemberCount}{' '}
|
|
<Plural value={memberCount} one="member" other="members" data-flx="channel.invite-embed.plural" />
|
|
</Trans>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
footer={
|
|
<Button
|
|
variant="primary"
|
|
fitContainer
|
|
matchSkeletonHeight
|
|
onClick={handleAcceptInvite}
|
|
disabled={isAlreadyInGroupDM}
|
|
data-flx="channel.invite-embed.button.accept-invite"
|
|
>
|
|
{isAlreadyInGroupDM ? i18n._(ALREADY_JOINED_DESCRIPTOR) : i18n._(JOIN_GROUP_DESCRIPTOR)}
|
|
</Button>
|
|
}
|
|
data-flx="channel.invite-embed.embed-card"
|
|
/>
|
|
);
|
|
} else if (isPackInviteGuard(invite)) {
|
|
const pack = invite.pack;
|
|
const packCreator = packCreatorRecord ?? new User(pack.creator);
|
|
const packKindLabel = pack.type === 'emoji' ? i18n._(EMOJI_PACK_DESCRIPTOR) : i18n._(STICKER_PACK_DESCRIPTOR);
|
|
const packActionLabel =
|
|
pack.type === 'emoji' ? i18n._(INSTALL_EMOJI_PACK_DESCRIPTOR) : i18n._(INSTALL_STICKER_PACK_DESCRIPTOR);
|
|
const inviterTag = invite.inviter
|
|
? NicknameUtils.formatTagForStreamerMode(`${invite.inviter.username}#${invite.inviter.discriminator}`)
|
|
: null;
|
|
const handleAcceptInvite = () => InviteCommands.acceptAndTransitionToChannel(invite.code, i18n);
|
|
content = (
|
|
<EmbedCard
|
|
splashURL={null}
|
|
headerClassName={styles.headerInvite}
|
|
icon={<Avatar user={packCreator} size={48} className={styles.icon} data-flx="channel.invite-embed.icon--2" />}
|
|
title={
|
|
<div
|
|
className={`${styles.titleContainer} ${styles.packTitleRow}`}
|
|
data-flx="channel.invite-embed.title-container--2"
|
|
>
|
|
<h3
|
|
className={`${cardStyles.title} ${cardStyles.titlePrimary} ${styles.titleText}`}
|
|
data-flx="channel.invite-embed.title-text--2"
|
|
>
|
|
{pack.name}
|
|
</h3>
|
|
<span className={styles.packBadge} data-flx="channel.invite-embed.pack-badge">
|
|
{packKindLabel}
|
|
</span>
|
|
</div>
|
|
}
|
|
body={
|
|
<div className={styles.packBody} data-flx="channel.invite-embed.pack-body">
|
|
<p className={styles.packDescription} data-flx="channel.invite-embed.pack-description">
|
|
{pack.description || i18n._(NO_DESCRIPTION_PROVIDED_DESCRIPTOR)}
|
|
</p>
|
|
<div className={styles.packMeta} data-flx="channel.invite-embed.pack-meta">
|
|
<span data-flx="channel.invite-embed.span">
|
|
<Trans>Created by {NicknameUtils.getDisplayName(packCreator)}</Trans>
|
|
</span>
|
|
{inviterTag ? (
|
|
<span data-flx="channel.invite-embed.span--2">
|
|
<Trans>Invited by {inviterTag}</Trans>
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<p className={styles.packNote} data-flx="channel.invite-embed.pack-note">
|
|
{i18n._(ACCEPTING_THIS_INVITE_INSTALLS_THE_PACK_AUTOMATICALLY_DESCRIPTOR)}
|
|
</p>
|
|
</div>
|
|
}
|
|
footer={
|
|
<Button
|
|
variant="primary"
|
|
fitContainer
|
|
matchSkeletonHeight
|
|
onClick={handleAcceptInvite}
|
|
data-flx="channel.invite-embed.button.accept-invite--2"
|
|
>
|
|
{packActionLabel}
|
|
</Button>
|
|
}
|
|
data-flx="channel.invite-embed.embed-card--2"
|
|
/>
|
|
);
|
|
} else if (!guild || !isGuildInvite(invite)) {
|
|
content = <InviteNotFoundError data-flx="channel.invite-embed.invite-not-found-error--2" />;
|
|
} else {
|
|
const guildActionState = getGuildInviteActionState({invite, guild});
|
|
const {features, isMember} = guildActionState;
|
|
const liveCounts = isMember ? GuildCount.getCounts(guild.id) : null;
|
|
const presenceCount = liveCounts?.onlineCount ?? guildActionState.presenceCount;
|
|
const memberCount = liveCounts?.memberCount ?? guildActionState.memberCount;
|
|
const splashAspectRatio = getGuildEmbedSplashAspectRatio(guild);
|
|
const renderedPresenceCount = formatInviteCount(presenceCount);
|
|
const renderedMemberCount = formatInviteCount(memberCount);
|
|
const handleAcceptInvite = () => InviteCommands.acceptAndTransitionToChannel(invite.code, i18n);
|
|
const guildPath = Routes.guildChannel(guild.id, invite.channel.id);
|
|
const handleNavigateToGuild = () => RouterUtils.transitionTo(guildPath);
|
|
const actionType = getGuildInvitePrimaryAction(guildActionState);
|
|
const isButtonDisabled = isGuildInviteActionDisabled(guildActionState);
|
|
const getButtonLabel = () => {
|
|
switch (actionType) {
|
|
case GuildInvitePrimaryAction.InvitesDisabled:
|
|
return i18n._(INVITES_DISABLED_DESCRIPTOR);
|
|
case GuildInvitePrimaryAction.GoToCommunity:
|
|
return i18n._(GO_TO_COMMUNITY_DESCRIPTOR);
|
|
default:
|
|
return i18n._(JOIN_COMMUNITY_DESCRIPTOR);
|
|
}
|
|
};
|
|
content = (
|
|
<EmbedCard
|
|
splashURL={splashURL}
|
|
splashAspectRatio={splashAspectRatio}
|
|
headerClassName={styles.headerInvite}
|
|
icon={
|
|
<GuildIcon
|
|
id={guild.id}
|
|
name={guild.name}
|
|
icon={guild.icon}
|
|
className={styles.icon}
|
|
data-flx="channel.invite-embed.icon--3"
|
|
/>
|
|
}
|
|
title={
|
|
<div className={styles.titleContainer} data-flx="channel.invite-embed.title-container--3">
|
|
<div className={styles.titleRowWithIcon} data-flx="channel.invite-embed.title-row-with-icon">
|
|
<h3
|
|
className={`${cardStyles.title} ${cardStyles.titlePrimary} ${styles.titleText}`}
|
|
data-flx="channel.invite-embed.title-text--3"
|
|
>
|
|
<FocusRing offset={-2} data-flx="channel.invite-embed.focus-ring--2">
|
|
<button
|
|
type="button"
|
|
className={cardStyles.titleButton}
|
|
onClick={handleNavigateToGuild}
|
|
onKeyDown={createTitleKeyDownHandler(handleNavigateToGuild)}
|
|
data-flx="channel.invite-embed.button.navigate-to-guild"
|
|
>
|
|
{guild.name}
|
|
</button>
|
|
</FocusRing>
|
|
</h3>
|
|
<GuildBadge
|
|
features={features}
|
|
variant="large"
|
|
onLightSurface
|
|
data-flx="channel.invite-embed.guild-badge"
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
body={
|
|
<div className={styles.stats} data-flx="channel.invite-embed.stats--2">
|
|
<div className={styles.stat} data-flx="channel.invite-embed.stat--2">
|
|
<div
|
|
className={`${styles.statDot} ${styles.statDotOnline}`}
|
|
data-flx="channel.invite-embed.stat-dot--2"
|
|
/>
|
|
<span className={styles.statText} data-flx="channel.invite-embed.stat-text--2">
|
|
{i18n._(ONLINE_DESCRIPTOR, {renderedPresenceCount})}
|
|
</span>
|
|
</div>
|
|
<div className={styles.stat} data-flx="channel.invite-embed.stat--3">
|
|
<div
|
|
className={`${styles.statDot} ${styles.statDotMembers}`}
|
|
data-flx="channel.invite-embed.stat-dot--3"
|
|
/>
|
|
<span className={styles.statText} data-flx="channel.invite-embed.stat-text--3">
|
|
<Trans>
|
|
{renderedMemberCount}{' '}
|
|
<Plural value={memberCount} one="member" other="members" data-flx="channel.invite-embed.plural--2" />
|
|
</Trans>
|
|
</span>
|
|
</div>
|
|
{actionType === GuildInvitePrimaryAction.InvitesDisabled && (
|
|
<p className={styles.statText} data-flx="channel.invite-embed.stat-text--4">
|
|
{guildActionState.isRaidDetected
|
|
? i18n._(DETECTED_A_POTENTIAL_RAID_SO_NEW_USERS_CAN_DESCRIPTOR, {productName: PRODUCT_NAME})
|
|
: i18n._(INVITES_ARE_CURRENTLY_PAUSED_FOR_THIS_COMMUNITY_DESCRIPTOR)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
}
|
|
footer={
|
|
<Button
|
|
variant="primary"
|
|
fitContainer
|
|
matchSkeletonHeight
|
|
onClick={handleAcceptInvite}
|
|
disabled={isButtonDisabled}
|
|
data-flx="channel.invite-embed.button.accept-invite--3"
|
|
>
|
|
{getButtonLabel()}
|
|
</Button>
|
|
}
|
|
data-flx="channel.invite-embed.embed-card--3"
|
|
/>
|
|
);
|
|
}
|
|
const handleContextMenu = (event: React.MouseEvent) => {
|
|
const channelId = invite && (isGuildInvite(invite) || isGroupDmInvite(invite)) ? invite.channel.id : null;
|
|
const guildForMenu = guild ? {id: guild.id, name: guild.name} : null;
|
|
const inviteCodeForMenu = guildForMenu && invite ? invite.code : null;
|
|
if (!message && !guildForMenu && !channelId) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
ContextMenuCommands.openFromEvent(event, (props) => (
|
|
<InviteEmbedContextMenu
|
|
message={message}
|
|
sourceChannel={sourceChannel}
|
|
linkUrl={`${RuntimeConfig.inviteEndpoint}/${code}`}
|
|
guild={guildForMenu}
|
|
channelId={channelId}
|
|
inviteCode={inviteCodeForMenu}
|
|
onDelete={onDelete}
|
|
onClose={props.onClose}
|
|
data-flx="channel.invite-embed.handle-context-menu.invite-embed-context-menu"
|
|
/>
|
|
));
|
|
};
|
|
return (
|
|
<div
|
|
ref={inviteWrapperRef}
|
|
role="group"
|
|
className={styles.inviteWrapper}
|
|
onContextMenu={handleContextMenu}
|
|
data-flx="channel.invite-embed.invite-wrapper.context-menu"
|
|
>
|
|
{content}
|
|
</div>
|
|
);
|
|
});
|
|
const InviteHiddenState = observer(() => {
|
|
const {i18n} = useLingui();
|
|
return (
|
|
<EmbedCard
|
|
splashURL={null}
|
|
headerClassName={styles.headerInvite}
|
|
icon={
|
|
<div className={styles.iconFallback} data-flx="channel.invite-embed.invite-hidden-state.icon-fallback">
|
|
<QuestionIcon weight="bold" data-flx="channel.invite-embed.invite-hidden-state.question-icon" />
|
|
</div>
|
|
}
|
|
title={
|
|
<div className={styles.titleContainer} data-flx="channel.invite-embed.invite-hidden-state.title-container">
|
|
<h3
|
|
className={`${cardStyles.title} ${cardStyles.titlePrimary} ${styles.titleText}`}
|
|
data-flx="channel.invite-embed.invite-hidden-state.title"
|
|
>
|
|
{i18n._(INVITE_MASKED_DESCRIPTOR)}
|
|
</h3>
|
|
</div>
|
|
}
|
|
body={
|
|
<p className={styles.statText} data-flx="channel.invite-embed.invite-hidden-state.body">
|
|
{i18n._(THIS_INVITE_IS_MASKED_WHILE_SHARING_DESCRIPTOR)}
|
|
</p>
|
|
}
|
|
footer={null}
|
|
data-flx="channel.invite-embed.invite-hidden-state.embed-card"
|
|
/>
|
|
);
|
|
});
|
|
const InviteLoadingState = observer(() => {
|
|
return (
|
|
<EmbedCard
|
|
splashURL={null}
|
|
headerClassName={styles.headerInvite}
|
|
icon={<EmbedSkeletonCircle data-flx="channel.invite-embed.invite-loading-state.embed-skeleton-circle" />}
|
|
title={
|
|
<div className={styles.titleContainer} data-flx="channel.invite-embed.invite-loading-state.title-container">
|
|
<div
|
|
className={styles.titleRowWithIcon}
|
|
data-flx="channel.invite-embed.invite-loading-state.title-row-with-icon"
|
|
>
|
|
<EmbedSkeletonTitle data-flx="channel.invite-embed.invite-loading-state.embed-skeleton-title" />
|
|
<EmbedSkeletonIcon data-flx="channel.invite-embed.invite-loading-state.embed-skeleton-icon" />
|
|
</div>
|
|
</div>
|
|
}
|
|
body={
|
|
<div className={styles.stats} data-flx="channel.invite-embed.invite-loading-state.stats">
|
|
<div className={styles.stat} data-flx="channel.invite-embed.invite-loading-state.stat">
|
|
<EmbedSkeletonDot data-flx="channel.invite-embed.invite-loading-state.embed-skeleton-dot" />
|
|
<EmbedSkeletonStatShort data-flx="channel.invite-embed.invite-loading-state.embed-skeleton-stat-short" />
|
|
</div>
|
|
<div className={styles.stat} data-flx="channel.invite-embed.invite-loading-state.stat--2">
|
|
<EmbedSkeletonDot data-flx="channel.invite-embed.invite-loading-state.embed-skeleton-dot--2" />
|
|
<EmbedSkeletonStatLong data-flx="channel.invite-embed.invite-loading-state.embed-skeleton-stat-long" />
|
|
</div>
|
|
</div>
|
|
}
|
|
footer={<EmbedSkeletonButton data-flx="channel.invite-embed.invite-loading-state.embed-skeleton-button" />}
|
|
data-flx="channel.invite-embed.invite-loading-state.embed-card"
|
|
/>
|
|
);
|
|
});
|
|
const InviteNotFoundError = observer(() => {
|
|
const {i18n} = useLingui();
|
|
return (
|
|
<EmbedCard
|
|
splashURL={null}
|
|
icon={
|
|
<div className={cardStyles.iconCircleDisabled} data-flx="channel.invite-embed.invite-not-found-error.div">
|
|
<QuestionIcon
|
|
className={cardStyles.iconError}
|
|
data-flx="channel.invite-embed.invite-not-found-error.question-icon"
|
|
/>
|
|
</div>
|
|
}
|
|
title={
|
|
<h3
|
|
className={`${cardStyles.title} ${cardStyles.titleDanger} ${styles.titleText}`}
|
|
data-flx="channel.invite-embed.invite-not-found-error.title-text"
|
|
>
|
|
{i18n._(UNKNOWN_INVITE_DESCRIPTOR)}
|
|
</h3>
|
|
}
|
|
subtitle={
|
|
<span className={cardStyles.helpText} data-flx="channel.invite-embed.invite-not-found-error.span">
|
|
{i18n._(TRY_ASKING_FOR_A_NEW_INVITE_DESCRIPTOR)}
|
|
</span>
|
|
}
|
|
footer={
|
|
<Button
|
|
variant="primary"
|
|
fitContainer
|
|
matchSkeletonHeight
|
|
disabled
|
|
data-flx="channel.invite-embed.invite-not-found-error.button"
|
|
>
|
|
{i18n._(INVITE_UNAVAILABLE_DESCRIPTOR)}
|
|
</Button>
|
|
}
|
|
data-flx="channel.invite-embed.invite-not-found-error.embed-card"
|
|
/>
|
|
);
|
|
});
|
|
|
|
interface GuildInviteEmbedPreviewProps {
|
|
guildId: string;
|
|
splashURLOverride?: string | null;
|
|
}
|
|
|
|
export const GuildInviteEmbedPreview = observer(function GuildInviteEmbedPreview({
|
|
guildId,
|
|
splashURLOverride,
|
|
}: GuildInviteEmbedPreviewProps) {
|
|
const {i18n} = useLingui();
|
|
const guild = Guilds.getGuild(guildId);
|
|
const [base64AspectRatio, setBase64AspectRatio] = useState<number | undefined>();
|
|
const splashAspectRatio = useMemo(() => {
|
|
if (!guild) return undefined;
|
|
if (splashURLOverride) {
|
|
return base64AspectRatio;
|
|
}
|
|
return getGuildEmbedSplashAspectRatio(guild);
|
|
}, [guild, splashURLOverride, base64AspectRatio]);
|
|
useEffect(() => {
|
|
if (splashURLOverride) {
|
|
getImageAspectRatioFromBase64(splashURLOverride)
|
|
.then((ratio) => setBase64AspectRatio(clampWideAssetAspectRatio(ratio)))
|
|
.catch(() => {
|
|
setBase64AspectRatio(undefined);
|
|
});
|
|
} else {
|
|
setBase64AspectRatio(undefined);
|
|
}
|
|
}, [splashURLOverride]);
|
|
useEffect(() => {
|
|
GuildCount.requestCounts(guildId);
|
|
}, [guildId]);
|
|
if (!guild) return null;
|
|
const splashURL =
|
|
splashURLOverride !== undefined
|
|
? splashURLOverride
|
|
: AvatarUtils.getGuildEmbedSplashURL({id: guild.id, embedSplash: guild.embedSplash || null});
|
|
const liveCounts = GuildCount.getCounts(guild.id);
|
|
const presenceCount = liveCounts?.onlineCount ?? 0;
|
|
const memberCount = liveCounts?.memberCount ?? 0;
|
|
const renderedPresenceCount = formatInviteCount(presenceCount);
|
|
const renderedMemberCount = formatInviteCount(memberCount);
|
|
return (
|
|
<EmbedCard
|
|
splashURL={splashURL}
|
|
splashAspectRatio={splashAspectRatio}
|
|
headerClassName={styles.headerInvite}
|
|
icon={
|
|
<GuildIcon
|
|
id={guild.id}
|
|
name={guild.name}
|
|
icon={guild.icon}
|
|
className={styles.icon}
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.icon"
|
|
/>
|
|
}
|
|
title={
|
|
<div
|
|
className={styles.titleContainer}
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.title-container"
|
|
>
|
|
<div
|
|
className={styles.titleRowWithIcon}
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.title-row-with-icon"
|
|
>
|
|
<h3
|
|
className={`${cardStyles.title} ${cardStyles.titlePrimary} ${styles.titleText}`}
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.title-text"
|
|
>
|
|
{guild.name}
|
|
</h3>
|
|
<GuildBadge
|
|
features={guild.features}
|
|
variant="large"
|
|
onLightSurface
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.guild-badge"
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
body={
|
|
<div className={styles.stats} data-flx="channel.invite-embed.guild-invite-embed-preview.stats">
|
|
<div className={styles.stat} data-flx="channel.invite-embed.guild-invite-embed-preview.stat">
|
|
<div
|
|
className={`${styles.statDot} ${styles.statDotOnline}`}
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.stat-dot"
|
|
/>
|
|
<span className={styles.statText} data-flx="channel.invite-embed.guild-invite-embed-preview.stat-text">
|
|
{i18n._(ONLINE_DESCRIPTOR, {renderedPresenceCount})}
|
|
</span>
|
|
</div>
|
|
<div className={styles.stat} data-flx="channel.invite-embed.guild-invite-embed-preview.stat--2">
|
|
<div
|
|
className={`${styles.statDot} ${styles.statDotMembers}`}
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.stat-dot--2"
|
|
/>
|
|
<span className={styles.statText} data-flx="channel.invite-embed.guild-invite-embed-preview.stat-text--2">
|
|
<Trans>
|
|
{renderedMemberCount}{' '}
|
|
<Plural
|
|
value={memberCount}
|
|
one="member"
|
|
other="members"
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.plural"
|
|
/>
|
|
</Trans>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
footer={
|
|
<Button
|
|
variant="primary"
|
|
fitContainer
|
|
matchSkeletonHeight
|
|
disabled
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.button"
|
|
>
|
|
{i18n._(JOIN_COMMUNITY_DESCRIPTOR)}
|
|
</Button>
|
|
}
|
|
data-flx="channel.invite-embed.guild-invite-embed-preview.embed-card"
|
|
/>
|
|
);
|
|
});
|