mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
572 lines
22 KiB
TypeScript
572 lines
22 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {showDmActionErrorModal} from '@app/features/app/components/alerts/DmActionErrorModal';
|
|
import {showGenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModalCommands';
|
|
import * as Modal from '@app/features/app/components/dialogs/Modal';
|
|
import {CopyLinkSection} from '@app/features/app/components/dialogs/shared/CopyLinkSection';
|
|
import {
|
|
RecipientList,
|
|
useRecipientItems,
|
|
useSendLinkToRecipients,
|
|
} from '@app/features/app/components/dialogs/shared/RecipientList';
|
|
import selectorStyles from '@app/features/app/components/dialogs/shared/SelectorModalStyles.module.css';
|
|
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
|
import type {Channel} from '@app/features/channel/models/Channel';
|
|
import Channels from '@app/features/channel/state/Channels';
|
|
import * as ChannelUtils from '@app/features/channel/utils/ChannelUtils';
|
|
import type {Guild} from '@app/features/guild/models/Guild';
|
|
import Guilds from '@app/features/guild/state/Guilds';
|
|
import {
|
|
CUSTOM_ELLIPSIS_DESCRIPTOR,
|
|
FAILED_TO_SEND_INVITE_DESCRIPTOR,
|
|
NEVER_DESCRIPTOR,
|
|
ONE_DAY_DURATION_DESCRIPTOR,
|
|
ONE_HOUR_DURATION_DESCRIPTOR,
|
|
SEARCH_FRIENDS_DESCRIPTOR,
|
|
SENT_DESCRIPTOR,
|
|
SEVEN_DAYS_DURATION_DESCRIPTOR,
|
|
SIX_HOURS_DURATION_DESCRIPTOR,
|
|
SOMETHING_WENT_WRONG_DESCRIPTOR,
|
|
THIRTY_MINUTES_DURATION_DESCRIPTOR,
|
|
TWELVE_HOURS_DURATION_DESCRIPTOR,
|
|
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
|
import * as InviteCommands from '@app/features/invite/commands/InviteCommands';
|
|
import styles from '@app/features/invite/components/modals/InviteModal.module.css';
|
|
import * as InviteUtils from '@app/features/invite/utils/InviteUtils';
|
|
import {failureCode} from '@app/features/platform/utils/ResponseInspection';
|
|
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
|
|
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
|
import {Button} from '@app/features/ui/button/Button';
|
|
import {Combobox} from '@app/features/ui/components/form/FormCombobox';
|
|
import {Input} from '@app/features/ui/components/form/FormInput';
|
|
import {Switch} from '@app/features/ui/components/form/FormSwitch';
|
|
import {Spinner} from '@app/features/ui/components/Spinner';
|
|
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
|
import {SteppedCarousel} from '@app/features/ui/stepped_carousel/SteppedCarousel';
|
|
import {useCopyLinkHandler} from '@app/lib/copy-link';
|
|
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
|
|
import {MAX_INVITE_AGE_SECONDS, MAX_INVITE_USES} from '@fluxer/constants/src/LimitConstants';
|
|
import type {Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
|
import {msg} from '@lingui/core/macro';
|
|
import {Trans, useLingui} from '@lingui/react/macro';
|
|
import {MagnifyingGlassIcon, WarningCircleIcon, WarningIcon} from '@phosphor-icons/react';
|
|
import clsx from 'clsx';
|
|
import {observer} from 'mobx-react-lite';
|
|
import {useCallback, useEffect, useMemo, useState} from 'react';
|
|
|
|
const NO_LIMIT_DESCRIPTOR = msg({
|
|
message: 'No limit',
|
|
comment: 'Invite max-uses option meaning unlimited uses. Short standalone label.',
|
|
});
|
|
const MESSAGE_1_USE_DESCRIPTOR = msg({
|
|
message: '1 use',
|
|
comment: 'Invite max-uses dropdown option for exactly one use.',
|
|
});
|
|
const MESSAGE_5_USES_DESCRIPTOR = msg({
|
|
message: '5 uses',
|
|
comment: 'Invite max-uses dropdown option for five uses.',
|
|
});
|
|
const MESSAGE_10_USES_DESCRIPTOR = msg({
|
|
message: '10 uses',
|
|
comment: 'Invite max-uses dropdown option for ten uses.',
|
|
});
|
|
const MESSAGE_25_USES_DESCRIPTOR = msg({
|
|
message: '25 uses',
|
|
comment: 'Invite max-uses dropdown option for 25 uses.',
|
|
});
|
|
const MESSAGE_50_USES_DESCRIPTOR = msg({
|
|
message: '50 uses',
|
|
comment: 'Invite max-uses dropdown option for 50 uses.',
|
|
});
|
|
const MESSAGE_100_USES_DESCRIPTOR = msg({
|
|
message: '100 uses',
|
|
comment: 'Invite max-uses dropdown option for 100 uses.',
|
|
});
|
|
const INVITE_FRIENDS_DESCRIPTOR = msg({
|
|
message: 'Invite friends',
|
|
comment:
|
|
'Fallback header on the invite modal when no community context is available. Refers to inviting friends to Fluxer.',
|
|
});
|
|
const INVITE_FRIENDS_TO_DESCRIPTOR = msg({
|
|
message: 'Invite friends to {guildName}',
|
|
comment: 'Title of the invite modal. {guildName} is the community name.',
|
|
});
|
|
const INVITE_LINK_SETTINGS_DESCRIPTOR = msg({
|
|
message: 'Invite link settings',
|
|
comment: 'Header of the invite modal when the user is editing the invite link expiry, max uses, and temporary flag.',
|
|
});
|
|
const INVITE_DESCRIPTOR = msg({
|
|
message: 'Invite',
|
|
comment:
|
|
'Per-recipient action button label in the invite modal recipient list. Sends the invite as a DM to that friend. Short standalone verb.',
|
|
});
|
|
const EXPIRE_AFTER_DESCRIPTOR = msg({
|
|
message: 'Expire after',
|
|
comment: 'Label above the expiration duration dropdown in the invite link settings.',
|
|
});
|
|
const CUSTOM_EXPIRY_SECONDS_DESCRIPTOR = msg({
|
|
message: 'Custom expiration (seconds)',
|
|
comment:
|
|
'Label of the numeric input shown after choosing "Custom..." in the invite expiration dropdown. The unit is seconds.',
|
|
});
|
|
const ANY_VALUE_FROM_0_TO_SECONDS_0_MEANS_DESCRIPTOR = msg({
|
|
message: 'Any value from 0 to {maxInviteAgeSeconds} seconds. 0 means never expires.',
|
|
comment:
|
|
'Helper text under the custom invite expiration input. {maxInviteAgeSeconds} is the platform max in seconds.',
|
|
});
|
|
const MAX_NUMBER_OF_USES_DESCRIPTOR = msg({
|
|
message: 'Max number of uses',
|
|
comment: 'Label above the max-uses dropdown in the invite link settings.',
|
|
});
|
|
const CUSTOM_MAX_USES_DESCRIPTOR = msg({
|
|
message: 'Custom max uses',
|
|
comment: 'Label of the numeric input shown after choosing "Custom..." in the invite max-uses dropdown.',
|
|
});
|
|
const ANY_VALUE_FROM_0_TO_0_MEANS_NO_DESCRIPTOR = msg({
|
|
message: 'Any value from 0 to {maxInviteUses}. 0 means no limit.',
|
|
comment:
|
|
'Helper text under the custom invite max-uses input. {maxInviteUses} is the platform max number of uses per invite.',
|
|
});
|
|
const GRANT_TEMPORARY_MEMBERSHIP_DESCRIPTOR = msg({
|
|
message: 'Temporary membership',
|
|
comment:
|
|
'Switch label in the invite link settings. When enabled, members joining via this invite are kicked when they go offline unless given a role.',
|
|
});
|
|
const MEMBERS_WILL_BE_REMOVED_WHEN_THEY_GO_OFFLINE_DESCRIPTOR = msg({
|
|
message: 'Members are removed when offline unless given a role.',
|
|
comment: 'Helper text under the temporary membership switch in the invite link settings.',
|
|
});
|
|
const INVITE_LINK_DESCRIPTOR = msg({
|
|
message: 'Invite link',
|
|
comment: 'Placeholder in the read-only invite link input when no link is available yet.',
|
|
});
|
|
const LINK_HIDDEN_WHILE_SHARING_DESCRIPTOR = msg({
|
|
message: 'Link hidden while sharing',
|
|
comment: 'Replacement text for an invite URL while streaming privacy is active.',
|
|
});
|
|
const INVITE_LINK_HIDDEN_LABEL_DESCRIPTOR = msg({
|
|
message: 'Invite link is hidden while sharing:',
|
|
comment: 'Label above a masked invite link while streaming privacy is active.',
|
|
});
|
|
const YOUR_INVITE_LINK_EXPIRES_IN_SECONDS_DESCRIPTOR = msg({
|
|
message:
|
|
'{seconds, plural, one {Your invite link expires in # second.} other {Your invite link expires in # seconds.}}',
|
|
comment: 'Text under a generated invite link. {seconds} is how long the link stays valid, in whole seconds.',
|
|
});
|
|
const YOUR_INVITE_LINK_EXPIRES_IN_MINUTES_DESCRIPTOR = msg({
|
|
message:
|
|
'{minutes, plural, one {Your invite link expires in # minute.} other {Your invite link expires in # minutes.}}',
|
|
comment: 'Text under a generated invite link. {minutes} is how long the link stays valid, in whole minutes.',
|
|
});
|
|
const YOUR_INVITE_LINK_EXPIRES_IN_HOURS_DESCRIPTOR = msg({
|
|
message: '{hours, plural, one {Your invite link expires in # hour.} other {Your invite link expires in # hours.}}',
|
|
comment: 'Text under a generated invite link. {hours} is how long the link stays valid, in whole hours.',
|
|
});
|
|
const YOUR_INVITE_LINK_EXPIRES_IN_DAYS_DESCRIPTOR = msg({
|
|
message: '{days, plural, one {Your invite link expires in # day.} other {Your invite link expires in # days.}}',
|
|
comment: 'Text under a generated invite link. {days} is how long the link stays valid, in whole days.',
|
|
});
|
|
|
|
type InviteModalView = 'recipients' | 'advanced';
|
|
|
|
const INVITE_MODAL_VIEW_ORDER: ReadonlyArray<InviteModalView> = ['recipients', 'advanced'];
|
|
|
|
interface InviteModalContentProps {
|
|
channelId: string;
|
|
channel: Channel;
|
|
guild: Guild;
|
|
inviteCapability: InviteUtils.InviteCapability;
|
|
}
|
|
|
|
const InviteModalUnavailable = observer(function InviteModalUnavailable() {
|
|
const {i18n} = useLingui();
|
|
return (
|
|
<Modal.Root size="small" centered data-flx="invite.invite-modal.modal-root">
|
|
<Modal.Header title={i18n._(INVITE_FRIENDS_DESCRIPTOR)} data-flx="invite.invite-modal.modal-header" />
|
|
<Modal.Content className={styles.noChannelContent} data-flx="invite.invite-modal.no-channel-content">
|
|
<WarningIcon
|
|
size={remFromPx(48)}
|
|
weight="fill"
|
|
className={styles.noChannelIcon}
|
|
data-flx="invite.invite-modal.no-channel-icon"
|
|
/>
|
|
<p className={styles.noChannelText} data-flx="invite.invite-modal.no-channel-text">
|
|
<Trans>There are no channels available to create an invite for.</Trans>
|
|
</p>
|
|
</Modal.Content>
|
|
</Modal.Root>
|
|
);
|
|
});
|
|
const InviteModalContent = observer(function InviteModalContent({
|
|
channelId,
|
|
channel,
|
|
guild,
|
|
inviteCapability,
|
|
}: InviteModalContentProps) {
|
|
const {i18n} = useLingui();
|
|
const isUsingVanityUrl = inviteCapability.useVanityUrl;
|
|
const [invite, setInvite] = useState<Invite | null>(null);
|
|
const [loading, setLoading] = useState(!isUsingVanityUrl);
|
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
|
|
const [maxAge, setMaxAge] = useState('604800');
|
|
const [maxUses, setMaxUses] = useState('0');
|
|
const [temporary, setTemporary] = useState(false);
|
|
const [isMaxAgeCustom, setIsMaxAgeCustom] = useState(false);
|
|
const [isMaxUsesCustom, setIsMaxUsesCustom] = useState(false);
|
|
const recipients = useRecipientItems();
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const CUSTOM_OPTION = '__custom__';
|
|
const maxAgeOptions = useMemo(
|
|
() => [
|
|
{value: '0', label: i18n._(NEVER_DESCRIPTOR)},
|
|
{value: '1800', label: i18n._(THIRTY_MINUTES_DURATION_DESCRIPTOR)},
|
|
{value: '3600', label: i18n._(ONE_HOUR_DURATION_DESCRIPTOR)},
|
|
{value: '21600', label: i18n._(SIX_HOURS_DURATION_DESCRIPTOR)},
|
|
{value: '43200', label: i18n._(TWELVE_HOURS_DURATION_DESCRIPTOR)},
|
|
{value: '86400', label: i18n._(ONE_DAY_DURATION_DESCRIPTOR)},
|
|
{value: '604800', label: i18n._(SEVEN_DAYS_DURATION_DESCRIPTOR)},
|
|
{value: CUSTOM_OPTION, label: i18n._(CUSTOM_ELLIPSIS_DESCRIPTOR)},
|
|
],
|
|
[i18n.locale],
|
|
);
|
|
const maxUsesOptions = useMemo(
|
|
() => [
|
|
{value: '0', label: i18n._(NO_LIMIT_DESCRIPTOR)},
|
|
{value: '1', label: i18n._(MESSAGE_1_USE_DESCRIPTOR)},
|
|
{value: '5', label: i18n._(MESSAGE_5_USES_DESCRIPTOR)},
|
|
{value: '10', label: i18n._(MESSAGE_10_USES_DESCRIPTOR)},
|
|
{value: '25', label: i18n._(MESSAGE_25_USES_DESCRIPTOR)},
|
|
{value: '50', label: i18n._(MESSAGE_50_USES_DESCRIPTOR)},
|
|
{value: '100', label: i18n._(MESSAGE_100_USES_DESCRIPTOR)},
|
|
{value: CUSTOM_OPTION, label: i18n._(CUSTOM_ELLIPSIS_DESCRIPTOR)},
|
|
],
|
|
[i18n.locale],
|
|
);
|
|
const clampNumber = (raw: string, max: number): string => {
|
|
if (raw === '') return '0';
|
|
const parsed = Number.parseInt(raw, 10);
|
|
if (Number.isNaN(parsed)) return '0';
|
|
return String(Math.max(0, Math.min(max, parsed)));
|
|
};
|
|
const loadInvite = useCallback(
|
|
async (options?: {maxAge?: number; maxUses?: number; temporary?: boolean}) => {
|
|
if (isUsingVanityUrl) {
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const newInvite = await InviteCommands.create(channelId, {
|
|
max_age: options?.maxAge,
|
|
max_uses: options?.maxUses,
|
|
temporary: options?.temporary,
|
|
});
|
|
setInvite(newInvite);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[channelId, isUsingVanityUrl],
|
|
);
|
|
useEffect(() => {
|
|
if (!isUsingVanityUrl) {
|
|
loadInvite({maxAge: 604800, maxUses: 0, temporary: false});
|
|
}
|
|
}, [loadInvite, isUsingVanityUrl]);
|
|
const title = i18n._(INVITE_FRIENDS_TO_DESCRIPTOR, {guildName: guild.name});
|
|
const invitesDisabled = guild.features.has(GuildFeatures.INVITES_DISABLED);
|
|
const inviteUrl = isUsingVanityUrl
|
|
? InviteUtils.getVanityInviteUrl(inviteCapability.vanityUrlCode!)
|
|
: invite
|
|
? `${RuntimeConfig.inviteEndpoint}/${invite.code}`
|
|
: '';
|
|
const hideInviteLinks = StreamerMode.shouldHideInviteLinks;
|
|
const displayedInviteUrl = hideInviteLinks && inviteUrl ? i18n._(LINK_HIDDEN_WHILE_SHARING_DESCRIPTOR) : inviteUrl;
|
|
const handleCopy = useCopyLinkHandler(inviteUrl, true);
|
|
const {
|
|
sendingTo,
|
|
sentTo: sentInvites,
|
|
send: handleSendInvite,
|
|
} = useSendLinkToRecipients(
|
|
() => inviteUrl,
|
|
(error: unknown) => {
|
|
if (failureCode(error)) {
|
|
showDmActionErrorModal(error);
|
|
return;
|
|
}
|
|
showGenericErrorModal({
|
|
title: () => i18n._(SOMETHING_WENT_WRONG_DESCRIPTOR),
|
|
message: () => i18n._(FAILED_TO_SEND_INVITE_DESCRIPTOR),
|
|
dataFlx: 'invite.invite-modal.send-invite-error-modal',
|
|
});
|
|
},
|
|
);
|
|
const handleGenerateNew = () => {
|
|
loadInvite({
|
|
maxAge: parseInt(maxAge, 10),
|
|
maxUses: parseInt(maxUses, 10),
|
|
temporary,
|
|
});
|
|
setShowAdvanced(false);
|
|
};
|
|
const getExpirationText = () => {
|
|
const seconds = Number.parseInt(maxAge, 10);
|
|
const minutes = seconds / 60;
|
|
const hours = minutes / 60;
|
|
const days = hours / 24;
|
|
if (days >= 1 && days % 1 === 0) {
|
|
return i18n._(YOUR_INVITE_LINK_EXPIRES_IN_DAYS_DESCRIPTOR, {days});
|
|
}
|
|
if (hours >= 1 && hours % 1 === 0) {
|
|
return i18n._(YOUR_INVITE_LINK_EXPIRES_IN_HOURS_DESCRIPTOR, {hours});
|
|
}
|
|
if (minutes >= 1 && minutes % 1 === 0) {
|
|
return i18n._(YOUR_INVITE_LINK_EXPIRES_IN_MINUTES_DESCRIPTOR, {minutes});
|
|
}
|
|
return i18n._(YOUR_INVITE_LINK_EXPIRES_IN_SECONDS_DESCRIPTOR, {seconds});
|
|
};
|
|
return (
|
|
<Modal.Root
|
|
size="small"
|
|
centered
|
|
className={selectorStyles.selectorRoot}
|
|
data-flx="invite.invite-modal.modal-root--2"
|
|
>
|
|
<Modal.Header
|
|
title={!showAdvanced ? title : i18n._(INVITE_LINK_SETTINGS_DESCRIPTOR)}
|
|
data-flx="invite.invite-modal.modal-header--2"
|
|
>
|
|
{!showAdvanced && (
|
|
<>
|
|
<p
|
|
className={clsx(selectorStyles.subtitle, styles.channelSubtitle)}
|
|
data-flx="invite.invite-modal.channel-subtitle"
|
|
>
|
|
<Trans>
|
|
Recipients will be taken to {ChannelUtils.getIcon(channel, {size: 16, className: styles.channelIcon})}{' '}
|
|
<span className={styles.channelName} data-flx="invite.invite-modal.channel-name">
|
|
{channel.name}
|
|
</span>
|
|
</Trans>
|
|
</p>
|
|
{invitesDisabled && (
|
|
<div className={styles.warningContainer} data-flx="invite.invite-modal.warning-container">
|
|
<WarningCircleIcon
|
|
className={styles.warningIcon}
|
|
weight="fill"
|
|
data-flx="invite.invite-modal.warning-icon"
|
|
/>
|
|
<p className={styles.warningText} data-flx="invite.invite-modal.warning-text">
|
|
<Trans>
|
|
Invites are paused in this community by an admin. While this invite can be created, it cannot be
|
|
accepted until invites are re-enabled.
|
|
</Trans>
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div className={selectorStyles.headerSearch} data-flx="invite.invite-modal.div">
|
|
<Input
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder={i18n._(SEARCH_FRIENDS_DESCRIPTOR)}
|
|
leftIcon={
|
|
<MagnifyingGlassIcon
|
|
size={remFromPx(20)}
|
|
weight="bold"
|
|
className={selectorStyles.searchIcon}
|
|
data-flx="invite.invite-modal.magnifying-glass-icon"
|
|
/>
|
|
}
|
|
className={selectorStyles.headerSearchInput}
|
|
data-flx="invite.invite-modal.input.set-search-query"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</Modal.Header>
|
|
<Modal.Content className={selectorStyles.selectorContent} data-flx="invite.invite-modal.modal-content">
|
|
{loading ? (
|
|
<div className={styles.loadingContainer} data-flx="invite.invite-modal.loading-container">
|
|
<Spinner data-flx="invite.invite-modal.spinner" />
|
|
</div>
|
|
) : (
|
|
<SteppedCarousel
|
|
step={showAdvanced ? 'advanced' : 'recipients'}
|
|
steps={INVITE_MODAL_VIEW_ORDER}
|
|
data-flx="invite.invite-modal.stepped-carousel"
|
|
>
|
|
{!showAdvanced ? (
|
|
<RecipientList
|
|
recipients={recipients}
|
|
sendingTo={sendingTo}
|
|
sentTo={sentInvites}
|
|
onSend={handleSendInvite}
|
|
defaultButtonLabel={i18n._(INVITE_DESCRIPTOR)}
|
|
sentButtonLabel={i18n._(SENT_DESCRIPTOR)}
|
|
buttonClassName={styles.inviteButton}
|
|
scrollerKey="invite-modal-friend-list-scroller"
|
|
searchQuery={searchQuery}
|
|
onSearchQueryChange={setSearchQuery}
|
|
showSearchInput={false}
|
|
data-flx="invite.invite-modal.recipient-list"
|
|
/>
|
|
) : (
|
|
<div className={styles.detailedView} data-flx="invite.invite-modal.advanced-view">
|
|
<Combobox
|
|
label={i18n._(EXPIRE_AFTER_DESCRIPTOR)}
|
|
options={maxAgeOptions}
|
|
value={isMaxAgeCustom ? CUSTOM_OPTION : maxAge}
|
|
onChange={(value) => {
|
|
if (value == null) return;
|
|
if (value === CUSTOM_OPTION) {
|
|
setIsMaxAgeCustom(true);
|
|
return;
|
|
}
|
|
setIsMaxAgeCustom(false);
|
|
setMaxAge(String(value));
|
|
}}
|
|
data-flx="invite.invite-modal.select.set-is-max-age-custom"
|
|
/>
|
|
{isMaxAgeCustom && (
|
|
<Input
|
|
type="number"
|
|
label={i18n._(CUSTOM_EXPIRY_SECONDS_DESCRIPTOR)}
|
|
footer={i18n._(ANY_VALUE_FROM_0_TO_SECONDS_0_MEANS_DESCRIPTOR, {
|
|
maxInviteAgeSeconds: MAX_INVITE_AGE_SECONDS,
|
|
})}
|
|
min={0}
|
|
max={MAX_INVITE_AGE_SECONDS}
|
|
step={1}
|
|
value={maxAge}
|
|
onChange={(event) => setMaxAge(clampNumber(event.target.value, MAX_INVITE_AGE_SECONDS))}
|
|
data-flx="invite.invite-modal.input.set-max-age.number"
|
|
/>
|
|
)}
|
|
<Combobox
|
|
label={i18n._(MAX_NUMBER_OF_USES_DESCRIPTOR)}
|
|
options={maxUsesOptions}
|
|
value={isMaxUsesCustom ? CUSTOM_OPTION : maxUses}
|
|
onChange={(value) => {
|
|
if (value == null) return;
|
|
if (value === CUSTOM_OPTION) {
|
|
setIsMaxUsesCustom(true);
|
|
return;
|
|
}
|
|
setIsMaxUsesCustom(false);
|
|
setMaxUses(String(value));
|
|
}}
|
|
data-flx="invite.invite-modal.select.set-is-max-uses-custom"
|
|
/>
|
|
{isMaxUsesCustom && (
|
|
<Input
|
|
type="number"
|
|
label={i18n._(CUSTOM_MAX_USES_DESCRIPTOR)}
|
|
footer={i18n._(ANY_VALUE_FROM_0_TO_0_MEANS_NO_DESCRIPTOR, {maxInviteUses: MAX_INVITE_USES})}
|
|
min={0}
|
|
max={MAX_INVITE_USES}
|
|
step={1}
|
|
value={maxUses}
|
|
onChange={(event) => setMaxUses(clampNumber(event.target.value, MAX_INVITE_USES))}
|
|
data-flx="invite.invite-modal.input.set-max-uses.number"
|
|
/>
|
|
)}
|
|
<Switch
|
|
label={i18n._(GRANT_TEMPORARY_MEMBERSHIP_DESCRIPTOR)}
|
|
description={i18n._(MEMBERS_WILL_BE_REMOVED_WHEN_THEY_GO_OFFLINE_DESCRIPTOR)}
|
|
value={temporary}
|
|
onChange={setTemporary}
|
|
data-flx="invite.invite-modal.switch.set-temporary"
|
|
/>
|
|
</div>
|
|
)}
|
|
</SteppedCarousel>
|
|
)}
|
|
</Modal.Content>
|
|
{!showAdvanced ? (
|
|
<Modal.Footer data-flx="invite.invite-modal.modal-footer--2">
|
|
<CopyLinkSection
|
|
label={
|
|
hideInviteLinks ? (
|
|
i18n._(INVITE_LINK_HIDDEN_LABEL_DESCRIPTOR)
|
|
) : (
|
|
<Trans>Or send an invite link to a friend:</Trans>
|
|
)
|
|
}
|
|
value={displayedInviteUrl}
|
|
onCopy={handleCopy}
|
|
copyDisabled={hideInviteLinks}
|
|
onInputClick={hideInviteLinks ? undefined : (e) => e.currentTarget.select()}
|
|
inputProps={{placeholder: i18n._(INVITE_LINK_DESCRIPTOR)}}
|
|
data-flx="invite.invite-modal.copy-link-section"
|
|
>
|
|
{isUsingVanityUrl || maxAge === '0' ? (
|
|
<p className={styles.expirationText} data-flx="invite.invite-modal.expiration-text">
|
|
<Trans>This invite link never expires.</Trans>{' '}
|
|
{!isUsingVanityUrl && (
|
|
<FocusRing offset={-2} data-flx="invite.invite-modal.focus-ring">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAdvanced(true)}
|
|
className={styles.editLink}
|
|
data-flx="invite.invite-modal.edit-link.set-show-advanced.button"
|
|
>
|
|
<Trans>Edit invite link</Trans>
|
|
</button>
|
|
</FocusRing>
|
|
)}
|
|
</p>
|
|
) : (
|
|
<p className={styles.expirationText} data-flx="invite.invite-modal.expiration-text--2">
|
|
{getExpirationText()}{' '}
|
|
<FocusRing offset={-2} data-flx="invite.invite-modal.focus-ring--2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAdvanced(true)}
|
|
className={styles.editLink}
|
|
data-flx="invite.invite-modal.edit-link.set-show-advanced.button--2"
|
|
>
|
|
<Trans>Edit invite link</Trans>
|
|
</button>
|
|
</FocusRing>
|
|
</p>
|
|
)}
|
|
</CopyLinkSection>
|
|
</Modal.Footer>
|
|
) : (
|
|
<Modal.Footer data-flx="invite.invite-modal.modal-footer--3">
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setShowAdvanced(false)}
|
|
data-flx="invite.invite-modal.button.set-show-advanced"
|
|
>
|
|
<Trans>Cancel</Trans>
|
|
</Button>
|
|
<Button onClick={handleGenerateNew} data-flx="invite.invite-modal.button.generate-new">
|
|
<Trans>Create new link</Trans>
|
|
</Button>
|
|
</Modal.Footer>
|
|
)}
|
|
</Modal.Root>
|
|
);
|
|
});
|
|
export const InviteModal = observer(({channelId}: {channelId: string}) => {
|
|
const channel = Channels.getChannel(channelId);
|
|
if (!channel || channel.guildId == null) {
|
|
return <InviteModalUnavailable data-flx="invite.invite-modal.invite-modal-unavailable" />;
|
|
}
|
|
const guild = Guilds.getGuild(channel.guildId);
|
|
if (!guild) {
|
|
return <InviteModalUnavailable data-flx="invite.invite-modal.invite-modal-unavailable--2" />;
|
|
}
|
|
const inviteCapability = InviteUtils.getInviteCapability(channelId, channel.guildId);
|
|
return (
|
|
<InviteModalContent
|
|
channelId={channelId}
|
|
channel={channel}
|
|
guild={guild}
|
|
inviteCapability={inviteCapability}
|
|
data-flx="invite.invite-modal.invite-modal-content"
|
|
/>
|
|
);
|
|
});
|