mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(expressions): remove the unfinished packs feature (#2250)
This commit is contained in:
1 parent
0eef611b6d
commit
b8f8d8d859
159 files changed
+42
-6863
No files matched your search
@@ -14,7 +14,6 @@ import {AppearanceInlineContent} from '@app/features/user/components/modals/tabs
|
||||
import ApplicationsTab from '@app/features/user/components/modals/tabs/applications_tab';
|
||||
import {ChatSettingsInlineContent} from '@app/features/user/components/modals/tabs/chat_settings_tab/ChatSettingsTabInline';
|
||||
import DesktopSettingsTab from '@app/features/user/components/modals/tabs/DesktopSettingsTab';
|
||||
import ExpressionPacksTab from '@app/features/user/components/modals/tabs/ExpressionPacksTab';
|
||||
import GiftInventoryTab from '@app/features/user/components/modals/tabs/GiftInventoryTab';
|
||||
import KeybindsTab from '@app/features/user/components/modals/tabs/KeybindsTab';
|
||||
import LanguageTab from '@app/features/user/components/modals/tabs/LanguageTab';
|
||||
@@ -65,7 +64,6 @@ const INLINE_TAB_COMPONENTS: Partial<Record<UserSettingsTabType, React.Component
|
||||
account_security: AccountSecurityInlineTab,
|
||||
plutonium: PlutoniumTab,
|
||||
gift_inventory: GiftInventoryTab,
|
||||
expression_packs: ExpressionPacksTab,
|
||||
privacy_safety: PrivacyDashboardContent,
|
||||
authorized_apps: AccountSecurityInlineTab,
|
||||
blocked_users: AccountSecurityInlineTab,
|
||||
|
||||
@@ -56,10 +56,6 @@ const FILE_UPLOAD_SIZE_DESCRIPTOR = msg({
|
||||
message: 'File upload size',
|
||||
comment: 'Feature comparison table perk label. Shown as the row name comparing restricted vs stock limits.',
|
||||
});
|
||||
const EMOJI_STICKER_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Emoji & sticker packs',
|
||||
comment: 'Feature comparison table perk label. Shown as the row name comparing restricted vs stock limits.',
|
||||
});
|
||||
const SAVED_MEDIA_DESCRIPTOR = msg({
|
||||
message: 'Saved media',
|
||||
comment: 'Feature comparison table perk label. Shown as the row name comparing restricted vs stock limits.',
|
||||
@@ -108,7 +104,6 @@ export const FeatureComparisonTable = observer(() => {
|
||||
message_character_limit: i18n._(MESSAGE_CHARACTER_LIMIT_DESCRIPTOR),
|
||||
bookmarked_messages: i18n._(BOOKMARKED_MESSAGES_DESCRIPTOR),
|
||||
file_upload_size: i18n._(FILE_UPLOAD_SIZE_DESCRIPTOR),
|
||||
emoji_sticker_packs: i18n._(EMOJI_STICKER_PACKS_DESCRIPTOR),
|
||||
saved_media: i18n._(SAVED_MEDIA_DESCRIPTOR),
|
||||
use_animated_emojis: i18n._(USE_ANIMATED_EMOJIS_DESCRIPTOR),
|
||||
global_emoji_sticker_access: i18n._(GLOBAL_EMOJI_STICKER_ACCESS_DESCRIPTOR),
|
||||
|
||||
@@ -222,17 +222,6 @@ export const Endpoints = {
|
||||
USER_PUSH_ROTATE: '/users/@me/push/rotate',
|
||||
USER_PUSH_SUBSCRIPTIONS: '/users/@me/push/subscriptions',
|
||||
USER_PUSH_SUBSCRIPTION: (subscriptionId: string) => `/users/@me/push/subscriptions/${subscriptionId}`,
|
||||
PACKS: '/packs',
|
||||
PACK: (packId: string) => `/packs/${packId}`,
|
||||
PACK_CREATE: (packType: 'emoji' | 'sticker') => `/packs/${packType}`,
|
||||
PACK_INSTALL: (packId: string) => `/packs/${packId}/install`,
|
||||
PACK_EMOJIS: (packId: string) => `/packs/emojis/${packId}`,
|
||||
PACK_EMOJI: (packId: string, emojiId: string) => `/packs/emojis/${packId}/${emojiId}`,
|
||||
PACK_EMOJI_BULK: (packId: string) => `/packs/emojis/${packId}/bulk`,
|
||||
PACK_STICKERS: (packId: string) => `/packs/stickers/${packId}`,
|
||||
PACK_STICKER: (packId: string, stickerId: string) => `/packs/stickers/${packId}/${stickerId}`,
|
||||
PACK_STICKERS_BULK: (packId: string) => `/packs/stickers/${packId}/bulk`,
|
||||
PACK_INVITES: (packId: string) => `/packs/${packId}/invites`,
|
||||
WEBHOOK: (webhookId: string) => `/webhooks/${webhookId}`,
|
||||
REPORT_MESSAGE: '/reports/message',
|
||||
REPORT_USER: '/reports/user',
|
||||
|
||||
@@ -79,15 +79,6 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.packBadge {
|
||||
background: var(--background-modifier-accent);
|
||||
border-radius: 62.4375rem;
|
||||
padding: 0.15rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.entityTitle {
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
@@ -107,24 +98,6 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.packDescription {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.packMeta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.packMetaText {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.entityStat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,39 +3,16 @@
|
||||
import styles from '@app/features/auth/flow/AuthPageStyles.module.css';
|
||||
import {GuildBadge} from '@app/features/guild/components/GuildBadge';
|
||||
import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon';
|
||||
import {NO_DESCRIPTION_PROVIDED_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {isGroupDmInvite, isGuildInvite, isPackInvite} from '@app/features/invite/types/InviteTypes';
|
||||
import {Avatar} from '@app/features/ui/components/Avatar';
|
||||
import {isGroupDmInvite, isGuildInvite} from '@app/features/invite/types/InviteTypes';
|
||||
import {BaseAvatar} from '@app/features/ui/components/BaseAvatar';
|
||||
import {User} from '@app/features/user/models/User';
|
||||
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 type {GroupDmInvite, GuildInvite, Invite, PackInvite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Plural, Trans, useLingui} from '@lingui/react/macro';
|
||||
import type {GroupDmInvite, GuildInvite, Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import {Plural, Trans} from '@lingui/react/macro';
|
||||
import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
|
||||
const EMOJI_PACK_DESCRIPTOR = msg({
|
||||
message: 'Emoji pack',
|
||||
comment: 'Short label in the authentication invite header. Keep the tone plain and specific.',
|
||||
});
|
||||
const STICKER_PACK_DESCRIPTOR = msg({
|
||||
message: 'Sticker pack',
|
||||
comment: 'Short label in the authentication invite header. Keep the tone plain and specific.',
|
||||
});
|
||||
const CREATED_BY_DESCRIPTOR = msg({
|
||||
message: 'Created by {userName}',
|
||||
comment:
|
||||
'Short label in the authentication invite header. Preserve {userName}; it is inserted by code. Keep the tone plain and specific.',
|
||||
});
|
||||
const INVITED_BY_DESCRIPTOR = msg({
|
||||
message: 'Invited by {inviterTag}',
|
||||
comment:
|
||||
'Short label in the authentication invite header. Preserve {inviterTag}; it is inserted by code. Keep the tone plain and specific.',
|
||||
});
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
interface InviteHeaderProps {
|
||||
invite: Invite;
|
||||
@@ -49,10 +26,6 @@ interface GroupDMInviteHeaderProps {
|
||||
invite: GroupDmInvite;
|
||||
}
|
||||
|
||||
interface PackInviteHeaderProps {
|
||||
invite: PackInvite;
|
||||
}
|
||||
|
||||
interface PreviewGuildInviteHeaderProps {
|
||||
guildId: string;
|
||||
guildName: string;
|
||||
@@ -175,69 +148,10 @@ export const GroupDMInviteHeader = observer(function GroupDMInviteHeader({invite
|
||||
</div>
|
||||
);
|
||||
});
|
||||
export const PackInviteHeader = observer(function PackInviteHeader({invite}: PackInviteHeaderProps) {
|
||||
const {i18n} = useLingui();
|
||||
const pack = invite.pack;
|
||||
const creatorRecord = useMemo(() => new User(pack.creator), [pack.creator]);
|
||||
const creatorDisplayName = NicknameUtils.getDisplayName(creatorRecord);
|
||||
const packKindLabel = pack.type === 'emoji' ? i18n._(EMOJI_PACK_DESCRIPTOR) : i18n._(STICKER_PACK_DESCRIPTOR);
|
||||
const inviterTag = invite.inviter ? `${invite.inviter.username}#${invite.inviter.discriminator}` : null;
|
||||
return (
|
||||
<div className={styles.entityHeader} data-flx="auth.flow.invite-header.pack-invite-header.entity-header">
|
||||
<div
|
||||
className={styles.entityIconWrapper}
|
||||
data-flx="auth.flow.invite-header.pack-invite-header.entity-icon-wrapper"
|
||||
>
|
||||
<Avatar
|
||||
user={creatorRecord}
|
||||
size={80}
|
||||
className={styles.entityIcon}
|
||||
data-flx="auth.flow.invite-header.pack-invite-header.entity-icon"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.entityDetails} data-flx="auth.flow.invite-header.pack-invite-header.entity-details">
|
||||
<p className={styles.entityText} data-flx="auth.flow.invite-header.pack-invite-header.entity-text">
|
||||
<Trans>You've been invited to install</Trans>
|
||||
</p>
|
||||
<div
|
||||
className={styles.entityTitleWrapper}
|
||||
data-flx="auth.flow.invite-header.pack-invite-header.entity-title-wrapper"
|
||||
>
|
||||
<h2 className={styles.entityTitle} data-flx="auth.flow.invite-header.pack-invite-header.entity-title">
|
||||
{pack.name}
|
||||
</h2>
|
||||
<span className={styles.packBadge} data-flx="auth.flow.invite-header.pack-invite-header.pack-badge">
|
||||
{packKindLabel}
|
||||
</span>
|
||||
</div>
|
||||
<p className={styles.packDescription} data-flx="auth.flow.invite-header.pack-invite-header.pack-description">
|
||||
{pack.description || i18n._(NO_DESCRIPTION_PROVIDED_DESCRIPTOR)}
|
||||
</p>
|
||||
<div className={styles.packMeta} data-flx="auth.flow.invite-header.pack-invite-header.pack-meta">
|
||||
<span className={styles.packMetaText} data-flx="auth.flow.invite-header.pack-invite-header.pack-meta-text">
|
||||
{i18n._(CREATED_BY_DESCRIPTOR, {userName: creatorDisplayName})}
|
||||
</span>
|
||||
{inviterTag ? (
|
||||
<span
|
||||
className={styles.packMetaText}
|
||||
data-flx="auth.flow.invite-header.pack-invite-header.pack-meta-text--2"
|
||||
>
|
||||
{i18n._(INVITED_BY_DESCRIPTOR, {inviterTag})}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export function InviteHeader({invite}: InviteHeaderProps) {
|
||||
if (isGroupDmInvite(invite)) {
|
||||
return <GroupDMInviteHeader invite={invite} data-flx="auth.flow.invite-header.group-dm-invite-header" />;
|
||||
}
|
||||
if (isPackInvite(invite)) {
|
||||
return <PackInviteHeader invite={invite} data-flx="auth.flow.invite-header.pack-invite-header" />;
|
||||
}
|
||||
if (isGuildInvite(invite)) {
|
||||
return <GuildInviteHeader invite={invite} data-flx="auth.flow.invite-header.guild-invite-header" />;
|
||||
}
|
||||
|
||||
@@ -105,45 +105,3 @@
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.packTitleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.packBadge {
|
||||
background: var(--background-modifier-accent);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.75em;
|
||||
line-height: 1.25;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.packBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.packDescription {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875em;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.packMeta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.78em;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.packNote {
|
||||
margin: 0;
|
||||
font-size: 0.75em;
|
||||
color: var(--text-tertiary-secondary);
|
||||
}
|
||||
@@ -16,18 +16,11 @@ 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 {JOIN_COMMUNITY_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 {isGroupDmInvite, isGuildInvite} from '@app/features/invite/types/InviteTypes';
|
||||
import {getGroupDmInviteCounts} from '@app/features/invite/utils/GroupDmInviteCounts';
|
||||
import {
|
||||
GuildInvitePrimaryAction,
|
||||
@@ -56,11 +49,9 @@ 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';
|
||||
@@ -81,26 +72,6 @@ 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.',
|
||||
@@ -185,12 +156,7 @@ const InviteEmbedInner = observer(function InviteEmbedInner({
|
||||
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;
|
||||
@@ -302,71 +268,6 @@ const InviteEmbedInner = observer(function InviteEmbedInner({
|
||||
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 {
|
||||
|
||||
-1
@@ -31,7 +31,6 @@ export const DEFAULT_DEVELOPER_OPTIONS = {
|
||||
selfHostedModeOverride: false,
|
||||
forceShowVanityURLDisclaimer: false,
|
||||
forceShowVoiceConnection: false,
|
||||
showExpressionPacksSettings: false,
|
||||
showProfileTimezoneSettings: false,
|
||||
premiumScenarioOverride: null,
|
||||
premiumTypeOverride: null,
|
||||
|
||||
@@ -122,15 +122,6 @@ const FORCE_SHOW_VOICE_CONNECTION_DESCRIPTOR = msg({
|
||||
message: 'Force show voice connection',
|
||||
comment: 'Developer option label for always showing the voice connection status bar.',
|
||||
});
|
||||
const SHOW_EXPRESSION_PACKS_SETTINGS_DESCRIPTOR = msg({
|
||||
message: 'Show expression packs settings',
|
||||
comment: 'Developer option label for exposing the staff-only Expression packs page in user settings.',
|
||||
});
|
||||
const SHOW_EXPRESSION_PACKS_SETTINGS_DESC_DESCRIPTOR = msg({
|
||||
message: 'Expose the staff-only Expression packs page in user settings.',
|
||||
comment:
|
||||
'Developer / debug surface — keep terse and technical. Tooltip / description for the Expression packs settings toggle.',
|
||||
});
|
||||
const SHOW_PROFILE_TIMEZONE_SETTINGS_DESCRIPTOR = msg({
|
||||
message: 'Show profile timezone settings',
|
||||
comment: 'Developer option label for exposing the staff-only profile timezone section in profile settings.',
|
||||
@@ -254,11 +245,6 @@ export const getToggleGroups = (): Array<ToggleGroup> => [
|
||||
label: FORCE_SHOW_VOICE_CONNECTION_DESCRIPTOR,
|
||||
description: ALWAYS_DISPLAY_THE_VOICE_CONNECTION_STATUS_BAR_IN_DESCRIPTOR,
|
||||
},
|
||||
{
|
||||
key: 'showExpressionPacksSettings',
|
||||
label: SHOW_EXPRESSION_PACKS_SETTINGS_DESCRIPTOR,
|
||||
description: SHOW_EXPRESSION_PACKS_SETTINGS_DESC_DESCRIPTOR,
|
||||
},
|
||||
{
|
||||
key: 'showProfileTimezoneSettings',
|
||||
label: SHOW_PROFILE_TIMEZONE_SETTINGS_DESCRIPTOR,
|
||||
|
||||
@@ -46,7 +46,6 @@ export type DeveloperOptionsState = Readonly<{
|
||||
selfHostedModeOverride: boolean;
|
||||
forceShowVanityURLDisclaimer: boolean;
|
||||
forceShowVoiceConnection: boolean;
|
||||
showExpressionPacksSettings: boolean;
|
||||
showProfileTimezoneSettings: boolean;
|
||||
premiumScenarioOverride: PremiumScenarioOverride | null;
|
||||
premiumTypeOverride: number | null;
|
||||
@@ -129,7 +128,6 @@ class DeveloperOptions implements DeveloperOptionsState {
|
||||
selfHostedModeOverride = false;
|
||||
forceShowVanityURLDisclaimer = false;
|
||||
forceShowVoiceConnection = false;
|
||||
showExpressionPacksSettings = false;
|
||||
showProfileTimezoneSettings = false;
|
||||
premiumScenarioOverride: PremiumScenarioOverride | null = null;
|
||||
premiumTypeOverride: number | null = null;
|
||||
@@ -215,7 +213,6 @@ class DeveloperOptions implements DeveloperOptionsState {
|
||||
'selfHostedModeOverride',
|
||||
'forceShowVanityURLDisclaimer',
|
||||
'forceShowVoiceConnection',
|
||||
'showExpressionPacksSettings',
|
||||
'showProfileTimezoneSettings',
|
||||
'premiumScenarioOverride',
|
||||
'premiumTypeOverride',
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Endpoints} from '@app/features/app/constants/Endpoints';
|
||||
import {http} from '@app/features/platform/transport/RestTransport';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import type {PackDashboardResponse, PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
|
||||
const logger = new Logger('Packs');
|
||||
|
||||
type PackType = 'emoji' | 'sticker';
|
||||
|
||||
interface PackCreateRequest {
|
||||
name: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
interface PackUpdateRequest {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
function packCreateRequest(name: string, description?: string | null): PackCreateRequest {
|
||||
return {name, description: description ?? null};
|
||||
}
|
||||
|
||||
export async function list(): Promise<PackDashboardResponse> {
|
||||
try {
|
||||
logger.debug('Requesting pack dashboard');
|
||||
const response = await http.get<PackDashboardResponse>(Endpoints.PACKS);
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch pack dashboard:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(type: PackType, name: string, description?: string | null): Promise<PackSummaryResponse> {
|
||||
try {
|
||||
logger.debug(`Creating ${type} pack ${name}`);
|
||||
const response = await http.post<PackSummaryResponse>(Endpoints.PACK_CREATE(type), {
|
||||
body: packCreateRequest(name, description),
|
||||
});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create ${type} pack:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(packId: string, data: PackUpdateRequest): Promise<PackSummaryResponse> {
|
||||
try {
|
||||
logger.debug(`Updating pack ${packId}`);
|
||||
const response = await http.patch<PackSummaryResponse>(Endpoints.PACK(packId), {body: data});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(packId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Deleting pack ${packId}`);
|
||||
await http.delete(Endpoints.PACK(packId));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function install(packId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Installing pack ${packId}`);
|
||||
await http.post(Endpoints.PACK_INSTALL(packId));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to install pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function uninstall(packId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Uninstalling pack ${packId}`);
|
||||
await http.delete(Endpoints.PACK_INSTALL(packId));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to uninstall pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.description {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.formFields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as Modal from '@app/features/app/components/dialogs/Modal';
|
||||
import {useFormSubmit} from '@app/features/app/hooks/useFormSubmit';
|
||||
import styles from '@app/features/expressions/components/modals/CreatePackModal.module.css';
|
||||
import Packs from '@app/features/expressions/state/ExpressionsPacks';
|
||||
import {CREATE_DESCRIPTOR, DESCRIPTION_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {Form} from '@app/features/ui/components/form/Form';
|
||||
import {Input, Textarea} from '@app/features/ui/components/form/FormInput';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {useCallback} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
|
||||
const CREATE_EMOJI_PACK_DESCRIPTOR = msg({
|
||||
message: 'Create emoji pack',
|
||||
comment: 'Action that opens the create-emoji-pack modal.',
|
||||
});
|
||||
const CREATE_STICKER_PACK_DESCRIPTOR = msg({
|
||||
message: 'Create sticker pack',
|
||||
comment: 'Action that opens the create-sticker-pack modal.',
|
||||
});
|
||||
const PACK_NAME_DESCRIPTOR = msg({
|
||||
message: 'Pack name',
|
||||
comment: 'Form field label for the name of an expression pack.',
|
||||
});
|
||||
const PACK_NAME_IS_REQUIRED_DESCRIPTOR = msg({
|
||||
message: 'Pack name is required',
|
||||
comment: 'Form validation error shown when the pack name field is empty.',
|
||||
});
|
||||
const PACK_NAME_MUST_BE_AT_LEAST_2_CHARACTERS_DESCRIPTOR = msg({
|
||||
message: 'Pack name must be at least 2 characters',
|
||||
comment: 'Form validation error for a pack name that is too short.',
|
||||
});
|
||||
const PACK_NAME_MUST_BE_AT_MOST_64_CHARACTERS_DESCRIPTOR = msg({
|
||||
message: 'Pack name must be at most 64 characters',
|
||||
comment: 'Form validation error for a pack name that is too long.',
|
||||
});
|
||||
const MY_SUPER_PACK_DESCRIPTOR = msg({
|
||||
message: 'My super pack',
|
||||
comment: 'Form placeholder example for a pack name input.',
|
||||
});
|
||||
const MAXIMUM_256_CHARACTERS_DESCRIPTOR = msg({
|
||||
message: 'Maximum 256 characters',
|
||||
comment: 'Form helper text describing the maximum length of a description field.',
|
||||
});
|
||||
const DESCRIBE_WHAT_EXPRESSIONS_ARE_INSIDE_THIS_PACK_DESCRIPTOR = msg({
|
||||
message: "What's in this pack?",
|
||||
comment: 'Form helper text for an expression pack description input.',
|
||||
});
|
||||
|
||||
interface FormInputs {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface CreatePackModalProps {
|
||||
type: 'emoji' | 'sticker';
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export const CreatePackModal = observer(({type, onSuccess}: CreatePackModalProps) => {
|
||||
const {i18n} = useLingui();
|
||||
const form = useForm<FormInputs>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
});
|
||||
const title = type === 'emoji' ? i18n._(CREATE_EMOJI_PACK_DESCRIPTOR) : i18n._(CREATE_STICKER_PACK_DESCRIPTOR);
|
||||
const submitHandler = useCallback(
|
||||
async (data: FormInputs) => {
|
||||
await Packs.createPack(type, data.name.trim(), data.description.trim() || null);
|
||||
onSuccess?.();
|
||||
ModalCommands.pop();
|
||||
},
|
||||
[type, onSuccess],
|
||||
);
|
||||
const {handleSubmit, isSubmitting} = useFormSubmit({
|
||||
form,
|
||||
onSubmit: submitHandler,
|
||||
defaultErrorField: 'name',
|
||||
});
|
||||
return (
|
||||
<Modal.Root size="small" onClose={() => ModalCommands.pop()} data-flx="expressions.create-pack-modal.modal-root">
|
||||
<Modal.Header title={title} data-flx="expressions.create-pack-modal.modal-header" />
|
||||
<Modal.Content data-flx="expressions.create-pack-modal.modal-content">
|
||||
<p className={styles.description} data-flx="expressions.create-pack-modal.description">
|
||||
{type === 'emoji' ? (
|
||||
<Trans>Start curating a custom emoji pack that you can share and install.</Trans>
|
||||
) : (
|
||||
<Trans>Bundle your favorite stickers into a pack you can distribute.</Trans>
|
||||
)}
|
||||
</p>
|
||||
<Form
|
||||
className={styles.form}
|
||||
form={form}
|
||||
onSubmit={handleSubmit}
|
||||
data-flx="expressions.create-pack-modal.form.submit"
|
||||
>
|
||||
<div className={styles.formFields} data-flx="expressions.create-pack-modal.form-fields">
|
||||
<Input
|
||||
id="pack-name"
|
||||
label={i18n._(PACK_NAME_DESCRIPTOR)}
|
||||
error={form.formState.errors.name?.message}
|
||||
data-flx="expressions.create-pack-modal.pack-name"
|
||||
{...form.register('name', {
|
||||
required: i18n._(PACK_NAME_IS_REQUIRED_DESCRIPTOR),
|
||||
minLength: {value: 2, message: i18n._(PACK_NAME_MUST_BE_AT_LEAST_2_CHARACTERS_DESCRIPTOR)},
|
||||
maxLength: {value: 64, message: i18n._(PACK_NAME_MUST_BE_AT_MOST_64_CHARACTERS_DESCRIPTOR)},
|
||||
})}
|
||||
placeholder={i18n._(MY_SUPER_PACK_DESCRIPTOR)}
|
||||
/>
|
||||
<Textarea
|
||||
id="pack-description"
|
||||
label={i18n._(DESCRIPTION_DESCRIPTOR)}
|
||||
error={form.formState.errors.description?.message}
|
||||
data-flx="expressions.create-pack-modal.pack-description"
|
||||
{...form.register('description', {
|
||||
maxLength: {value: 256, message: i18n._(MAXIMUM_256_CHARACTERS_DESCRIPTOR)},
|
||||
})}
|
||||
placeholder={i18n._(DESCRIBE_WHAT_EXPRESSIONS_ARE_INSIDE_THIS_PACK_DESCRIPTOR)}
|
||||
minRows={3}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Footer data-flx="expressions.create-pack-modal.modal-footer">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => ModalCommands.pop()}
|
||||
data-flx="expressions.create-pack-modal.button.pop"
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} submitting={isSubmitting} data-flx="expressions.create-pack-modal.button.submit">
|
||||
{i18n._(CREATE_DESCRIPTOR)}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Root>
|
||||
);
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as Modal from '@app/features/app/components/dialogs/Modal';
|
||||
import {useFormSubmit} from '@app/features/app/hooks/useFormSubmit';
|
||||
import styles from '@app/features/expressions/components/modals/CreatePackModal.module.css';
|
||||
import Packs from '@app/features/expressions/state/ExpressionsPacks';
|
||||
import {DESCRIPTION_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {Form} from '@app/features/ui/components/form/Form';
|
||||
import {Input, Textarea} from '@app/features/ui/components/form/FormInput';
|
||||
import type {PackType} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {useCallback} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
|
||||
const EDIT_EMOJI_PACK_DESCRIPTOR = msg({
|
||||
message: 'Edit emoji pack',
|
||||
comment: 'Action that opens the edit-emoji-pack modal.',
|
||||
});
|
||||
const EDIT_STICKER_PACK_DESCRIPTOR = msg({
|
||||
message: 'Edit sticker pack',
|
||||
comment: 'Action that opens the edit-sticker-pack modal.',
|
||||
});
|
||||
const PACK_NAME_DESCRIPTOR = msg({
|
||||
message: 'Pack name',
|
||||
comment: 'Form field label for the name of an expression pack.',
|
||||
});
|
||||
const PACK_NAME_IS_REQUIRED_DESCRIPTOR = msg({
|
||||
message: 'Pack name is required',
|
||||
comment: 'Form validation error shown when the pack name field is empty.',
|
||||
});
|
||||
const PACK_NAME_MUST_BE_AT_LEAST_2_CHARACTERS_DESCRIPTOR = msg({
|
||||
message: 'Pack name must be at least 2 characters',
|
||||
comment: 'Form validation error for a pack name that is too short.',
|
||||
});
|
||||
const PACK_NAME_MUST_BE_AT_MOST_64_CHARACTERS_DESCRIPTOR = msg({
|
||||
message: 'Pack name must be at most 64 characters',
|
||||
comment: 'Form validation error for a pack name that is too long.',
|
||||
});
|
||||
const MAXIMUM_256_CHARACTERS_DESCRIPTOR = msg({
|
||||
message: 'Maximum 256 characters',
|
||||
comment: 'Form helper text describing the maximum length of a description field.',
|
||||
});
|
||||
|
||||
interface FormInputs {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface EditPackModalProps {
|
||||
packId: string;
|
||||
type: PackType;
|
||||
name: string;
|
||||
description: string | null;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export const EditPackModal = observer(({packId, type, name, description, onSuccess}: EditPackModalProps) => {
|
||||
const {i18n} = useLingui();
|
||||
const form = useForm<FormInputs>({
|
||||
defaultValues: {
|
||||
name,
|
||||
description: description ?? '',
|
||||
},
|
||||
});
|
||||
const title = type === 'emoji' ? i18n._(EDIT_EMOJI_PACK_DESCRIPTOR) : i18n._(EDIT_STICKER_PACK_DESCRIPTOR);
|
||||
const submitHandler = useCallback(
|
||||
async (data: FormInputs) => {
|
||||
await Packs.updatePack(packId, {name: data.name.trim(), description: data.description.trim() || null});
|
||||
onSuccess?.();
|
||||
ModalCommands.pop();
|
||||
},
|
||||
[packId, onSuccess],
|
||||
);
|
||||
const {handleSubmit, isSubmitting} = useFormSubmit({
|
||||
form,
|
||||
onSubmit: submitHandler,
|
||||
defaultErrorField: 'name',
|
||||
});
|
||||
return (
|
||||
<Modal.Root size="small" onClose={() => ModalCommands.pop()} data-flx="expressions.edit-pack-modal.modal-root">
|
||||
<Modal.Header title={title} data-flx="expressions.edit-pack-modal.modal-header" />
|
||||
<Modal.Content data-flx="expressions.edit-pack-modal.modal-content">
|
||||
<Form
|
||||
className={styles.form}
|
||||
form={form}
|
||||
onSubmit={handleSubmit}
|
||||
data-flx="expressions.edit-pack-modal.form.submit"
|
||||
>
|
||||
<div className={styles.formFields} data-flx="expressions.edit-pack-modal.form-fields">
|
||||
<Input
|
||||
id="pack-name"
|
||||
label={i18n._(PACK_NAME_DESCRIPTOR)}
|
||||
error={form.formState.errors.name?.message}
|
||||
data-flx="expressions.edit-pack-modal.pack-name"
|
||||
{...form.register('name', {
|
||||
required: i18n._(PACK_NAME_IS_REQUIRED_DESCRIPTOR),
|
||||
minLength: {value: 2, message: i18n._(PACK_NAME_MUST_BE_AT_LEAST_2_CHARACTERS_DESCRIPTOR)},
|
||||
maxLength: {value: 64, message: i18n._(PACK_NAME_MUST_BE_AT_MOST_64_CHARACTERS_DESCRIPTOR)},
|
||||
})}
|
||||
/>
|
||||
<Textarea
|
||||
id="pack-description"
|
||||
label={i18n._(DESCRIPTION_DESCRIPTOR)}
|
||||
error={form.formState.errors.description?.message}
|
||||
data-flx="expressions.edit-pack-modal.pack-description"
|
||||
{...form.register('description', {
|
||||
maxLength: {value: 256, message: i18n._(MAXIMUM_256_CHARACTERS_DESCRIPTOR)},
|
||||
})}
|
||||
minRows={3}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Footer data-flx="expressions.edit-pack-modal.modal-footer">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => ModalCommands.pop()}
|
||||
data-flx="expressions.edit-pack-modal.button.pop"
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} submitting={isSubmitting} data-flx="expressions.edit-pack-modal.button.submit">
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Root>
|
||||
);
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.description {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.fieldGroup {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.helpText {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as Modal from '@app/features/app/components/dialogs/Modal';
|
||||
import {CopyLinkSection} from '@app/features/app/components/dialogs/shared/CopyLinkSection';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import styles from '@app/features/expressions/components/modals/PackInviteModal.module.css';
|
||||
import {
|
||||
CLOSE_DESCRIPTOR,
|
||||
NEVER_DESCRIPTOR,
|
||||
ONE_DAY_DURATION_DESCRIPTOR,
|
||||
ONE_HOUR_DURATION_DESCRIPTOR,
|
||||
SEVEN_DAYS_DURATION_DESCRIPTOR,
|
||||
SIX_HOURS_DURATION_DESCRIPTOR,
|
||||
THIRTY_MINUTES_DURATION_DESCRIPTOR,
|
||||
TWELVE_HOURS_DURATION_DESCRIPTOR,
|
||||
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import * as PackInviteCommands from '@app/features/invite/commands/PackInviteCommands';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {Combobox, type ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {Switch} from '@app/features/ui/components/form/FormSwitch';
|
||||
import {useCopyLinkHandler} from '@app/lib/copy-link';
|
||||
import type {PackType} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {useId, useMemo, useState} from 'react';
|
||||
|
||||
const UNLIMITED_DESCRIPTOR = msg({
|
||||
message: 'Unlimited',
|
||||
comment: 'Option label representing an unlimited count.',
|
||||
});
|
||||
const MESSAGE_1_USE_DESCRIPTOR = msg({
|
||||
message: '1 use',
|
||||
comment: 'Invite link use-limit option meaning the invite can be used once.',
|
||||
});
|
||||
const MESSAGE_5_USES_DESCRIPTOR = msg({
|
||||
message: '5 uses',
|
||||
comment: 'Invite link use-limit option meaning the invite can be used five times.',
|
||||
});
|
||||
const MESSAGE_10_USES_DESCRIPTOR = msg({
|
||||
message: '10 uses',
|
||||
comment: 'Invite link use-limit option meaning the invite can be used ten times.',
|
||||
});
|
||||
const MESSAGE_25_USES_DESCRIPTOR = msg({
|
||||
message: '25 uses',
|
||||
comment: 'Invite link use-limit option meaning the invite can be used 25 times.',
|
||||
});
|
||||
const MESSAGE_50_USES_DESCRIPTOR = msg({
|
||||
message: '50 uses',
|
||||
comment: 'Invite link use-limit option meaning the invite can be used 50 times.',
|
||||
});
|
||||
const MESSAGE_100_USES_DESCRIPTOR = msg({
|
||||
message: '100 uses',
|
||||
comment: 'Invite link use-limit option meaning the invite can be used 100 times.',
|
||||
});
|
||||
const EMOJI_PACK_INVITE_DESCRIPTOR = msg({
|
||||
message: 'Emoji pack invite',
|
||||
comment: 'Modal title for the emoji pack invite share flow.',
|
||||
});
|
||||
const STICKER_PACK_INVITE_DESCRIPTOR = msg({
|
||||
message: 'Sticker pack invite',
|
||||
comment: 'Modal title for the sticker pack invite share flow.',
|
||||
});
|
||||
const SEND_A_LINK_TO_LET_OTHERS_INSTALL_YOUR_DESCRIPTOR = msg({
|
||||
message: 'Share this link — the pack installs when accepted.',
|
||||
comment: 'Description shown in the emoji pack invite modal.',
|
||||
});
|
||||
const SHARE_YOUR_STICKER_PACK_WITH_OTHERS_VIA_A_DESCRIPTOR = msg({
|
||||
message: 'Share your sticker pack via link.',
|
||||
comment: 'Description shown in the sticker pack invite modal.',
|
||||
});
|
||||
const TOGGLE_UNIQUE_INVITE_DESCRIPTOR = msg({
|
||||
message: 'Toggle unique invite',
|
||||
comment: 'Action that toggles whether the pack invite is unique to the recipient.',
|
||||
});
|
||||
|
||||
interface PackInviteModalProps {
|
||||
packId: string;
|
||||
type: PackType;
|
||||
onCreated?: () => void;
|
||||
}
|
||||
|
||||
export const PackInviteModal = observer(({packId, type, onCreated}: PackInviteModalProps) => {
|
||||
const {i18n} = useLingui();
|
||||
const MAX_AGE_OPTIONS: Array<ComboboxOption<string>> = 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)},
|
||||
],
|
||||
[i18n.locale],
|
||||
);
|
||||
const MAX_USES_OPTIONS: Array<ComboboxOption<string>> = useMemo(
|
||||
() => [
|
||||
{value: '0', label: i18n._(UNLIMITED_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)},
|
||||
],
|
||||
[i18n.locale],
|
||||
);
|
||||
const [maxAge, setMaxAge] = useState('0');
|
||||
const [maxUses, setMaxUses] = useState('0');
|
||||
const [unique, setUnique] = useState(false);
|
||||
const [inviteCode, setInviteCode] = useState<string | null>(null);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const maxAgeSelectId = useId();
|
||||
const maxUsesSelectId = useId();
|
||||
const title = type === 'emoji' ? i18n._(EMOJI_PACK_INVITE_DESCRIPTOR) : i18n._(STICKER_PACK_INVITE_DESCRIPTOR);
|
||||
const description =
|
||||
type === 'emoji'
|
||||
? i18n._(SEND_A_LINK_TO_LET_OTHERS_INSTALL_YOUR_DESCRIPTOR)
|
||||
: i18n._(SHARE_YOUR_STICKER_PACK_WITH_OTHERS_VIA_A_DESCRIPTOR);
|
||||
const inviteUrl = inviteCode ? `${RuntimeConfig.inviteEndpoint}/${inviteCode}` : '';
|
||||
const handleGenerateInvite = async () => {
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const metadata = await PackInviteCommands.createInvite({
|
||||
packId,
|
||||
maxAge: parseInt(maxAge, 10),
|
||||
maxUses: parseInt(maxUses, 10),
|
||||
unique,
|
||||
});
|
||||
setInviteCode(metadata.code);
|
||||
onCreated?.();
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
const handleCopy = useCopyLinkHandler(inviteUrl, true);
|
||||
return (
|
||||
<Modal.Root size="small" onClose={() => ModalCommands.pop()} data-flx="expressions.pack-invite-modal.modal-root">
|
||||
<Modal.Header title={title} data-flx="expressions.pack-invite-modal.modal-header" />
|
||||
<Modal.Content data-flx="expressions.pack-invite-modal.modal-content">
|
||||
<p className={styles.description} data-flx="expressions.pack-invite-modal.description">
|
||||
{description}
|
||||
</p>
|
||||
<div className={styles.fieldGroup} data-flx="expressions.pack-invite-modal.field-group">
|
||||
<label
|
||||
htmlFor={maxAgeSelectId}
|
||||
className={styles.fieldLabel}
|
||||
data-flx="expressions.pack-invite-modal.field-label"
|
||||
>
|
||||
<Trans>Expiration</Trans>
|
||||
</label>
|
||||
<Combobox
|
||||
id={maxAgeSelectId}
|
||||
value={maxAge}
|
||||
options={MAX_AGE_OPTIONS}
|
||||
onChange={(value) => setMaxAge(value)}
|
||||
data-flx="expressions.pack-invite-modal.select.set-max-age"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.fieldGroup} data-flx="expressions.pack-invite-modal.field-group--2">
|
||||
<label
|
||||
htmlFor={maxUsesSelectId}
|
||||
className={styles.fieldLabel}
|
||||
data-flx="expressions.pack-invite-modal.field-label--2"
|
||||
>
|
||||
<Trans>Max uses</Trans>
|
||||
</label>
|
||||
<Combobox
|
||||
id={maxUsesSelectId}
|
||||
value={maxUses}
|
||||
options={MAX_USES_OPTIONS}
|
||||
onChange={(value) => setMaxUses(value)}
|
||||
data-flx="expressions.pack-invite-modal.select.set-max-uses"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.fieldGroup} data-flx="expressions.pack-invite-modal.field-group--3">
|
||||
<Switch
|
||||
label={<Trans>Unique invite</Trans>}
|
||||
value={unique}
|
||||
onChange={(value) => setUnique(value)}
|
||||
ariaLabel={i18n._(TOGGLE_UNIQUE_INVITE_DESCRIPTOR)}
|
||||
data-flx="expressions.pack-invite-modal.switch.set-unique"
|
||||
/>
|
||||
<p className={styles.helpText} data-flx="expressions.pack-invite-modal.help-text">
|
||||
<Trans>Each link works once.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
{inviteCode && (
|
||||
<CopyLinkSection
|
||||
label={<Trans>Share this link</Trans>}
|
||||
value={inviteUrl}
|
||||
onCopy={handleCopy}
|
||||
placeholder={`${RuntimeConfig.inviteEndpoint}/...`}
|
||||
data-flx="expressions.pack-invite-modal.copy-link-section"
|
||||
/>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Footer data-flx="expressions.pack-invite-modal.modal-footer">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => ModalCommands.pop()}
|
||||
data-flx="expressions.pack-invite-modal.button.pop"
|
||||
>
|
||||
{i18n._(CLOSE_DESCRIPTOR)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleGenerateInvite}
|
||||
submitting={isCreating}
|
||||
data-flx="expressions.pack-invite-modal.button.generate-invite"
|
||||
>
|
||||
<Trans>Generate invite</Trans>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Root>
|
||||
);
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as PackCommands from '@app/features/expressions/commands/PackCommands';
|
||||
import type {PackDashboardResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
import {makeAutoObservable, runInAction} from 'mobx';
|
||||
|
||||
type FetchStatus = 'idle' | 'pending' | 'success' | 'error';
|
||||
|
||||
class Packs {
|
||||
dashboard: PackDashboardResponse | null = null;
|
||||
fetchStatus: FetchStatus = 'idle';
|
||||
error: Error | null = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {}, {autoBind: true});
|
||||
}
|
||||
|
||||
async fetch(): Promise<PackDashboardResponse> {
|
||||
if (this.fetchStatus === 'pending') {
|
||||
throw new Error('Pack fetch already in progress');
|
||||
}
|
||||
this.fetchStatus = 'pending';
|
||||
this.error = null;
|
||||
try {
|
||||
const dashboard = await PackCommands.list();
|
||||
runInAction(() => {
|
||||
this.dashboard = dashboard;
|
||||
this.fetchStatus = 'success';
|
||||
});
|
||||
return dashboard;
|
||||
} catch (err) {
|
||||
runInAction(() => {
|
||||
this.fetchStatus = 'error';
|
||||
this.error = err instanceof Error ? err : new Error('Failed to load packs');
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
await this.fetch();
|
||||
}
|
||||
|
||||
async createPack(type: 'emoji' | 'sticker', name: string, description?: string | null): Promise<void> {
|
||||
await PackCommands.create(type, name, description);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async updatePack(
|
||||
packId: string,
|
||||
data: {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
},
|
||||
): Promise<void> {
|
||||
await PackCommands.update(packId, data);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async deletePack(packId: string): Promise<void> {
|
||||
await PackCommands.remove(packId);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async installPack(packId: string): Promise<void> {
|
||||
await PackCommands.install(packId);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async uninstallPack(packId: string): Promise<void> {
|
||||
await PackCommands.uninstall(packId);
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
export default new Packs();
|
||||
@@ -2686,9 +2686,6 @@
|
||||
{
|
||||
"msgid": "Options"
|
||||
},
|
||||
{
|
||||
"msgid": "Packs"
|
||||
},
|
||||
{
|
||||
"msgid": "Page {pageNumber}"
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ import {InviteAcceptFailedModal} from '@app/features/invite/components/alerts/In
|
||||
import {InvitesDisabledModal} from '@app/features/invite/components/alerts/InvitesDisabledModal';
|
||||
import {InviteAcceptModal} from '@app/features/invite/components/modals/InviteAcceptModal';
|
||||
import Invites from '@app/features/invite/state/Invites';
|
||||
import {isGroupDmInvite, isGuildInvite, isPackInvite} from '@app/features/invite/types/InviteTypes';
|
||||
import {isGroupDmInvite, isGuildInvite} from '@app/features/invite/types/InviteTypes';
|
||||
import GuildMembers from '@app/features/member/state/GuildMembers';
|
||||
import {UserBannedFromGuildModal} from '@app/features/moderation/components/alerts/UserBannedFromGuildModal';
|
||||
import {UserIpBannedFromGuildModal} from '@app/features/moderation/components/alerts/UserIpBannedFromGuildModal';
|
||||
@@ -20,10 +20,9 @@ import * as NavigationCommands from '@app/features/navigation/commands/Navigatio
|
||||
import {http} from '@app/features/platform/transport/RestTransport';
|
||||
import {HttpError} from '@app/features/platform/types/EndpointError';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {failureCode, failureMessage} from '@app/features/platform/utils/ResponseInspection';
|
||||
import {failureCode} from '@app/features/platform/utils/ResponseInspection';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||
@@ -31,8 +30,7 @@ import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
|
||||
import type {Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg, plural} from '@lingui/core/macro';
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
const ACCOUNT_VERIFICATION_REQUIRED_DESCRIPTOR = msg({
|
||||
message: 'Account verification required',
|
||||
@@ -44,74 +42,6 @@ const PLEASE_VERIFY_YOUR_ACCOUNT_BY_SETTING_AN_EMAIL_DESCRIPTOR = msg({
|
||||
comment:
|
||||
'Body of the error modal shown when an unclaimed (guest) account tries to accept a community invite. Tells the user to complete sign-up first.',
|
||||
});
|
||||
const EMOJI_PACK_LIMIT_REACHED_DESCRIPTOR = msg({
|
||||
message: 'Emoji pack limit reached',
|
||||
comment:
|
||||
'Title of the error modal when installing an emoji pack via invite is rejected because the user is at the install limit.',
|
||||
});
|
||||
const YOU_HAVE_REACHED_THE_LIMIT_FOR_INSTALLING_EMOJI_DESCRIPTOR = msg({
|
||||
message: "You're at the install limit. Remove one to add another.",
|
||||
comment: 'Fallback body of the emoji pack install-limit error modal when no exact limit is known.',
|
||||
});
|
||||
const EMOJI_PACK_CREATION_LIMIT_REACHED_DESCRIPTOR = msg({
|
||||
message: 'Emoji pack creation limit reached',
|
||||
comment:
|
||||
'Title of the error modal when creating a new emoji pack is rejected because the user is at the creation limit.',
|
||||
});
|
||||
const YOU_HAVE_REACHED_THE_LIMIT_FOR_CREATING_EMOJI_DESCRIPTOR = msg({
|
||||
message: "You're at the creation limit. Delete one to create another.",
|
||||
comment: 'Fallback body of the emoji pack creation-limit error modal when no exact limit is known.',
|
||||
});
|
||||
const STICKER_PACK_LIMIT_REACHED_DESCRIPTOR = msg({
|
||||
message: 'Sticker pack limit reached',
|
||||
comment:
|
||||
'Title of the error modal when installing a sticker pack via invite is rejected because the user is at the install limit.',
|
||||
});
|
||||
const YOU_HAVE_REACHED_THE_LIMIT_FOR_INSTALLING_STICKER_DESCRIPTOR = msg({
|
||||
message: "You're at the install limit. Remove one to add another.",
|
||||
comment: 'Fallback body of the sticker pack install-limit error modal when no exact limit is known.',
|
||||
});
|
||||
const STICKER_PACK_CREATION_LIMIT_REACHED_DESCRIPTOR = msg({
|
||||
message: 'Sticker pack creation limit reached',
|
||||
comment:
|
||||
'Title of the error modal when creating a new sticker pack is rejected because the user is at the creation limit.',
|
||||
});
|
||||
const YOU_HAVE_REACHED_THE_LIMIT_FOR_CREATING_STICKER_DESCRIPTOR = msg({
|
||||
message: "You're at the creation limit. Delete one to create another.",
|
||||
comment: 'Fallback body of the sticker pack creation-limit error modal when no exact limit is known.',
|
||||
});
|
||||
const CANNOT_INSTALL_EMOJI_PACK_DESCRIPTOR = msg({
|
||||
message: 'Cannot install emoji pack',
|
||||
comment: 'Title of the error modal when installing an emoji pack via invite is rejected for missing permissions.',
|
||||
});
|
||||
const CANNOT_INSTALL_STICKER_PACK_DESCRIPTOR = msg({
|
||||
message: 'Cannot install sticker pack',
|
||||
comment: 'Title of the error modal when installing a sticker pack via invite is rejected for missing permissions.',
|
||||
});
|
||||
const YOU_DON_T_HAVE_PERMISSION_TO_INSTALL_THIS_DESCRIPTOR = msg({
|
||||
message: "You can't install this emoji pack.",
|
||||
comment: 'Body of the error modal when installing an emoji pack via invite is rejected for missing permissions.',
|
||||
});
|
||||
const YOU_DON_T_HAVE_PERMISSION_TO_INSTALL_THIS_2_DESCRIPTOR = msg({
|
||||
message: "You can't install this sticker pack.",
|
||||
comment: 'Body of the error modal when installing a sticker pack via invite is rejected for missing permissions.',
|
||||
});
|
||||
const UNABLE_TO_INSTALL_EMOJI_PACK_DESCRIPTOR = msg({
|
||||
message: 'Unable to install emoji pack',
|
||||
comment: 'Generic title for the error modal when an emoji pack invite fails with an unrecognized error code.',
|
||||
});
|
||||
const UNABLE_TO_INSTALL_STICKER_PACK_DESCRIPTOR = msg({
|
||||
message: 'Unable to install sticker pack',
|
||||
comment: 'Generic title for the error modal when a sticker pack invite fails with an unrecognized error code.',
|
||||
});
|
||||
const FAILED_TO_INSTALL_THIS_EMOJI_PACK_PLEASE_TRY_DESCRIPTOR = msg({
|
||||
message: "Couldn't install this emoji pack. Try again later.",
|
||||
comment: 'Generic body for the error modal when an emoji pack invite fails with an unrecognized error code.',
|
||||
});
|
||||
const FAILED_TO_INSTALL_THIS_STICKER_PACK_PLEASE_TRY_DESCRIPTOR = msg({
|
||||
message: "Couldn't install this sticker pack. Try again later.",
|
||||
comment: 'Generic body for the error modal when a sticker pack invite fails with an unrecognized error code.',
|
||||
});
|
||||
const logger = new Logger('Invites');
|
||||
const ACCEPT_INVITE_BODY = {} as Invite;
|
||||
const isUnclaimedAccountInviteError = (code?: string): boolean => {
|
||||
@@ -133,21 +63,6 @@ function guildInviteFeatures(invite: Invite | null): Array<string> {
|
||||
return invite && isGuildInvite(invite) && Array.isArray(invite.guild.features) ? invite.guild.features : [];
|
||||
}
|
||||
|
||||
function showPackInstalledToast(invite: Invite): void {
|
||||
if (!isPackInvite(invite)) {
|
||||
return;
|
||||
}
|
||||
ToastCommands.createToast({
|
||||
type: 'success',
|
||||
children:
|
||||
invite.pack.type === 'emoji' ? (
|
||||
<Trans>Emoji pack {invite.pack.name} has been installed.</Trans>
|
||||
) : (
|
||||
<Trans>Sticker pack {invite.pack.name} has been installed.</Trans>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function removeInviteIfMissing(code: string, responseErr: HttpError | null, errorCode?: string): void {
|
||||
if (responseErr?.status === 404 || errorCode === APIErrorCodes.UNKNOWN_INVITE) {
|
||||
logger.debug(`Invite ${code} not found, removing from store`);
|
||||
@@ -283,11 +198,6 @@ export async function acceptAndTransitionToChannel(code: string, i18n: I18n): Pr
|
||||
if (!invite) {
|
||||
throw new Error(`Invite ${code} returned no data`);
|
||||
}
|
||||
if (isPackInvite(invite)) {
|
||||
await accept(code);
|
||||
showPackInstalledToast(invite);
|
||||
return;
|
||||
}
|
||||
if (isGroupDmInvite(invite)) {
|
||||
const channelId = invite.channel.id;
|
||||
logger.debug(`Accepting group DM invite ${code} and opening channel ${channelId}`);
|
||||
@@ -296,7 +206,7 @@ export async function acceptAndTransitionToChannel(code: string, i18n: I18n): Pr
|
||||
return;
|
||||
}
|
||||
if (!isGuildInvite(invite)) {
|
||||
throw new Error(`Invite ${code} is not a guild, group DM, or pack invite`);
|
||||
throw new Error(`Invite ${code} is not a guild or group DM invite`);
|
||||
}
|
||||
const channelId = invite.channel.id;
|
||||
const inviteTargetAllowed = shouldOpenInviteGuildChannel(invite.channel.type);
|
||||
@@ -324,9 +234,6 @@ export async function acceptAndTransitionToChannel(code: string, i18n: I18n): Pr
|
||||
const errorCode = failureCode(error);
|
||||
logger.error(`Failed to accept invite and transition for code ${code}:`, error);
|
||||
removeInviteIfMissing(code, responseErr, errorCode);
|
||||
if (handlePackInviteError({invite, errorCode, responseErr, i18n})) {
|
||||
throw error;
|
||||
}
|
||||
showGuildInviteAcceptFailure(i18n, invite, errorCode, responseErr);
|
||||
throw error;
|
||||
}
|
||||
@@ -342,169 +249,6 @@ export async function openAcceptModal(code: string): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
interface HandlePackInviteErrorParams {
|
||||
invite: Invite | null;
|
||||
errorCode?: string;
|
||||
responseErr?: HttpError | null;
|
||||
i18n: I18n;
|
||||
}
|
||||
|
||||
interface PackLimitPayload {
|
||||
packType?: 'emoji' | 'sticker';
|
||||
limit?: number;
|
||||
action?: 'create' | 'install';
|
||||
}
|
||||
|
||||
const getPackLimitPayload = (responseErr?: HttpError | null): PackLimitPayload | null => {
|
||||
const body = responseErr?.body;
|
||||
if (!body || typeof body !== 'object') return null;
|
||||
const record = body as Record<string, unknown>;
|
||||
const data = record.data;
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
const dataRecord = data as Record<string, unknown>;
|
||||
const limit = dataRecord.limit;
|
||||
const packType = dataRecord.pack_type;
|
||||
const action = dataRecord.action;
|
||||
return {
|
||||
packType: packType === 'emoji' || packType === 'sticker' ? packType : undefined,
|
||||
limit: typeof limit === 'number' ? limit : undefined,
|
||||
action: action === 'create' || action === 'install' ? action : undefined,
|
||||
};
|
||||
};
|
||||
const buildPackLimitStrings = (
|
||||
i18n: I18n,
|
||||
packType: 'emoji' | 'sticker',
|
||||
action: 'install' | 'create',
|
||||
limit?: number,
|
||||
): {title: string; message: string} => {
|
||||
switch (packType) {
|
||||
case 'emoji': {
|
||||
switch (action) {
|
||||
case 'install': {
|
||||
const title = i18n._(EMOJI_PACK_LIMIT_REACHED_DESCRIPTOR);
|
||||
const message =
|
||||
typeof limit === 'number'
|
||||
? plural(
|
||||
{count: limit},
|
||||
{
|
||||
one: "You're at the limit of # installed emoji pack. Remove one to add another.",
|
||||
other: "You're at the limit of # installed emoji packs. Remove one to add another.",
|
||||
},
|
||||
)
|
||||
: i18n._(YOU_HAVE_REACHED_THE_LIMIT_FOR_INSTALLING_EMOJI_DESCRIPTOR);
|
||||
return {title, message};
|
||||
}
|
||||
default: {
|
||||
const title = i18n._(EMOJI_PACK_CREATION_LIMIT_REACHED_DESCRIPTOR);
|
||||
const message =
|
||||
typeof limit === 'number'
|
||||
? plural(
|
||||
{count: limit},
|
||||
{
|
||||
one: "You're at the limit of # emoji pack. Delete one to create another.",
|
||||
other: "You're at the limit of # emoji packs. Delete one to create another.",
|
||||
},
|
||||
)
|
||||
: i18n._(YOU_HAVE_REACHED_THE_LIMIT_FOR_CREATING_EMOJI_DESCRIPTOR);
|
||||
return {title, message};
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
switch (action) {
|
||||
case 'install': {
|
||||
const title = i18n._(STICKER_PACK_LIMIT_REACHED_DESCRIPTOR);
|
||||
const message =
|
||||
typeof limit === 'number'
|
||||
? plural(
|
||||
{count: limit},
|
||||
{
|
||||
one: "You're at the limit of # installed sticker pack. Remove one to add another.",
|
||||
other: "You're at the limit of # installed sticker packs. Remove one to add another.",
|
||||
},
|
||||
)
|
||||
: i18n._(YOU_HAVE_REACHED_THE_LIMIT_FOR_INSTALLING_STICKER_DESCRIPTOR);
|
||||
return {title, message};
|
||||
}
|
||||
default: {
|
||||
const title = i18n._(STICKER_PACK_CREATION_LIMIT_REACHED_DESCRIPTOR);
|
||||
const message =
|
||||
typeof limit === 'number'
|
||||
? plural(
|
||||
{count: limit},
|
||||
{
|
||||
one: "You're at the limit of # sticker pack. Delete one to create another.",
|
||||
other: "You're at the limit of # sticker packs. Delete one to create another.",
|
||||
},
|
||||
)
|
||||
: i18n._(YOU_HAVE_REACHED_THE_LIMIT_FOR_CREATING_STICKER_DESCRIPTOR);
|
||||
return {title, message};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function handlePackInviteError(params: HandlePackInviteErrorParams): boolean {
|
||||
const {invite, errorCode, responseErr, i18n} = params;
|
||||
if (!invite || !isPackInvite(invite)) {
|
||||
return false;
|
||||
}
|
||||
const isEmojiPack = invite.pack.type === 'emoji';
|
||||
const cannotInstallTitle = isEmojiPack
|
||||
? i18n._(CANNOT_INSTALL_EMOJI_PACK_DESCRIPTOR)
|
||||
: i18n._(CANNOT_INSTALL_STICKER_PACK_DESCRIPTOR);
|
||||
const cannotInstallMessage = isEmojiPack
|
||||
? i18n._(YOU_DON_T_HAVE_PERMISSION_TO_INSTALL_THIS_DESCRIPTOR)
|
||||
: i18n._(YOU_DON_T_HAVE_PERMISSION_TO_INSTALL_THIS_2_DESCRIPTOR);
|
||||
const defaultTitle = isEmojiPack
|
||||
? i18n._(UNABLE_TO_INSTALL_EMOJI_PACK_DESCRIPTOR)
|
||||
: i18n._(UNABLE_TO_INSTALL_STICKER_PACK_DESCRIPTOR);
|
||||
const defaultMessage = isEmojiPack
|
||||
? i18n._(FAILED_TO_INSTALL_THIS_EMOJI_PACK_PLEASE_TRY_DESCRIPTOR)
|
||||
: i18n._(FAILED_TO_INSTALL_THIS_STICKER_PACK_PLEASE_TRY_DESCRIPTOR);
|
||||
if (errorCode === APIErrorCodes.MISSING_ACCESS) {
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<GenericErrorModal
|
||||
title={cannotInstallTitle}
|
||||
message={cannotInstallMessage}
|
||||
data-flx="invite.invite-commands.handle-pack-invite-error.generic-error-modal"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (errorCode === APIErrorCodes.MAX_PACKS) {
|
||||
const payload = getPackLimitPayload(responseErr);
|
||||
const packType = payload?.packType ?? invite.pack.type;
|
||||
const action = payload?.action ?? 'install';
|
||||
const limit = payload?.limit;
|
||||
const {title, message} = buildPackLimitStrings(i18n, packType, action, limit);
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<GenericErrorModal
|
||||
title={title}
|
||||
message={message}
|
||||
data-flx="invite.invite-commands.handle-pack-invite-error.generic-error-modal--2"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
const fallbackMessage = responseErr ? failureMessage(responseErr) : null;
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<GenericErrorModal
|
||||
title={defaultTitle}
|
||||
message={fallbackMessage || defaultMessage}
|
||||
data-flx="invite.invite-commands.handle-pack-invite-error.generic-error-modal--3"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function create(
|
||||
channelId: string,
|
||||
params?: {max_age?: number; max_uses?: number; temporary?: boolean},
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Endpoints} from '@app/features/app/constants/Endpoints';
|
||||
import {http} from '@app/features/platform/transport/RestTransport';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import type {PackInviteMetadataResponse} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
|
||||
const logger = new Logger('PackInvites');
|
||||
|
||||
export interface CreatePackInviteParams {
|
||||
packId: string;
|
||||
maxUses?: number;
|
||||
maxAge?: number;
|
||||
unique?: boolean;
|
||||
}
|
||||
|
||||
interface PackInviteRequestBody {
|
||||
max_uses: number;
|
||||
max_age: number;
|
||||
unique: boolean;
|
||||
}
|
||||
|
||||
function packInviteRequestBody(params: CreatePackInviteParams): PackInviteRequestBody {
|
||||
return {
|
||||
max_uses: params.maxUses ?? 0,
|
||||
max_age: params.maxAge ?? 0,
|
||||
unique: params.unique ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestPackInvite(params: CreatePackInviteParams): Promise<PackInviteMetadataResponse> {
|
||||
const response = await http.post<PackInviteMetadataResponse>(Endpoints.PACK_INVITES(params.packId), {
|
||||
body: packInviteRequestBody(params),
|
||||
});
|
||||
return response.body;
|
||||
}
|
||||
|
||||
function rethrowPackInviteFailure(packId: string, error: unknown): never {
|
||||
logger.error(`Failed to create invite for pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
export async function createInvite(params: CreatePackInviteParams): Promise<PackInviteMetadataResponse> {
|
||||
try {
|
||||
logger.debug(`Creating invite for pack ${params.packId}`);
|
||||
return await requestPackInvite(params);
|
||||
} catch (error) {
|
||||
rethrowPackInviteFailure(params.packId, error);
|
||||
}
|
||||
}
|
||||
@@ -100,31 +100,6 @@
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.packDescriptionText {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.packMetaRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.packMetaText {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.packNote {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-tertiary-secondary);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.root {
|
||||
width: calc(100vw - 1.5rem);
|
||||
|
||||
@@ -5,18 +5,11 @@ import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import {AuthErrorState} from '@app/features/auth/flow/AuthErrorState';
|
||||
import {AuthLoadingState} from '@app/features/auth/flow/AuthLoadingState';
|
||||
import {InviteHeader} from '@app/features/auth/flow/InviteHeader';
|
||||
import {
|
||||
JOIN_COMMUNITY_DESCRIPTOR,
|
||||
NO_DESCRIPTION_PROVIDED_DESCRIPTOR,
|
||||
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {JOIN_COMMUNITY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import * as InviteCommands from '@app/features/invite/commands/InviteCommands';
|
||||
import styles from '@app/features/invite/components/modals/InviteAcceptModal.module.css';
|
||||
import Invites from '@app/features/invite/state/Invites';
|
||||
import {
|
||||
isGroupDmInvite,
|
||||
isGuildInvite,
|
||||
isPackInvite as isPackInviteGuard,
|
||||
} from '@app/features/invite/types/InviteTypes';
|
||||
import {isGroupDmInvite, isGuildInvite} from '@app/features/invite/types/InviteTypes';
|
||||
import {getGroupDmInviteCounts} from '@app/features/invite/utils/GroupDmInviteCounts';
|
||||
import {
|
||||
GuildInvitePrimaryAction,
|
||||
@@ -35,7 +28,6 @@ import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
|
||||
import foodPatternUrl from '@app/media/images/i-like-food.svg';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
@@ -54,34 +46,6 @@ const GO_TO_COMMUNITY_DESCRIPTOR = msg({
|
||||
message: 'Go to community',
|
||||
comment: 'Short label in the invite accept modal. Keep it concise.',
|
||||
});
|
||||
const EMOJI_PACK_DESCRIPTOR = msg({
|
||||
message: 'Emoji pack',
|
||||
comment: 'Short label in the invite accept modal. Keep it concise.',
|
||||
});
|
||||
const STICKER_PACK_DESCRIPTOR = msg({
|
||||
message: 'Sticker pack',
|
||||
comment: 'Short label in the invite accept modal. Keep it concise.',
|
||||
});
|
||||
const INSTALL_EMOJI_PACK_DESCRIPTOR = msg({
|
||||
message: 'Install emoji pack',
|
||||
comment: 'Short label in the invite accept modal. Keep it concise.',
|
||||
});
|
||||
const INSTALL_STICKER_PACK_DESCRIPTOR = msg({
|
||||
message: 'Install sticker pack',
|
||||
comment: 'Short label in the invite accept modal. Keep it concise.',
|
||||
});
|
||||
const CREATED_BY_DESCRIPTOR = msg({
|
||||
message: 'Created by {userName}',
|
||||
comment: 'Metadata label for an expression pack invite. userName is the pack creator username.',
|
||||
});
|
||||
const INVITED_BY_DESCRIPTOR = msg({
|
||||
message: 'Invited by {userTag}',
|
||||
comment: 'Metadata label for an expression pack invite. userTag is the inviter username and discriminator.',
|
||||
});
|
||||
const ACCEPTING_INVITE_INSTALLS_PACK_DESCRIPTOR = msg({
|
||||
message: 'Accepting this invite installs the pack automatically.',
|
||||
comment: 'Note shown on expression pack invites before accepting.',
|
||||
});
|
||||
const logger = new Logger('InviteAcceptModal');
|
||||
|
||||
interface InviteAcceptModalProps {
|
||||
@@ -106,7 +70,6 @@ export const InviteAcceptModal = observer(function InviteAcceptModal({code}: Inv
|
||||
inviteMemberCount: invite.member_count,
|
||||
})
|
||||
: null;
|
||||
const isPackInvite = invite != null && isPackInviteGuard(invite);
|
||||
const guildActionState = getGuildInviteActionState({invite});
|
||||
const {presenceCount, memberCount} = guildActionState;
|
||||
const inviteForHeader = useMemo(() => {
|
||||
@@ -181,51 +144,6 @@ export const InviteAcceptModal = observer(function InviteAcceptModal({code}: Inv
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isPackInvite && invite) {
|
||||
const packKindLabel =
|
||||
invite.pack.type === 'emoji' ? i18n._(EMOJI_PACK_DESCRIPTOR) : i18n._(STICKER_PACK_DESCRIPTOR);
|
||||
const packActionLabel =
|
||||
invite.pack.type === 'emoji' ? i18n._(INSTALL_EMOJI_PACK_DESCRIPTOR) : i18n._(INSTALL_STICKER_PACK_DESCRIPTOR);
|
||||
const creatorUserName = NicknameUtils.getDisplayName(invite.pack.creator);
|
||||
const inviterTag = invite.inviter ? `${invite.inviter.username}#${invite.inviter.discriminator}` : null;
|
||||
return (
|
||||
<div className={styles.cardInner} data-flx="invite.invite-accept-modal.render-body.card-inner">
|
||||
<InviteHeader invite={inviteForHeader} data-flx="invite.invite-accept-modal.render-body.invite-header" />
|
||||
<p
|
||||
className={styles.packDescriptionText}
|
||||
data-flx="invite.invite-accept-modal.render-body.pack-description-text"
|
||||
>
|
||||
{invite.pack.description || i18n._(NO_DESCRIPTION_PROVIDED_DESCRIPTOR)}
|
||||
</p>
|
||||
<div className={styles.packMetaRow} data-flx="invite.invite-accept-modal.render-body.pack-meta-row">
|
||||
<span className={styles.packMetaText} data-flx="invite.invite-accept-modal.render-body.pack-meta-text">
|
||||
{packKindLabel}
|
||||
</span>
|
||||
<span className={styles.packMetaText} data-flx="invite.invite-accept-modal.render-body.pack-meta-text--2">
|
||||
{i18n._(CREATED_BY_DESCRIPTOR, {userName: creatorUserName})}
|
||||
</span>
|
||||
{inviterTag ? (
|
||||
<span className={styles.packMetaText} data-flx="invite.invite-accept-modal.render-body.pack-meta-text--3">
|
||||
{i18n._(INVITED_BY_DESCRIPTOR, {userTag: inviterTag})}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className={styles.packNote} data-flx="invite.invite-accept-modal.render-body.pack-note">
|
||||
{i18n._(ACCEPTING_INVITE_INSTALLS_PACK_DESCRIPTOR)}
|
||||
</p>
|
||||
<div className={styles.actions} data-flx="invite.invite-accept-modal.render-body.actions">
|
||||
<Button
|
||||
onClick={handleAccept}
|
||||
disabled={isAccepting}
|
||||
submitting={isAccepting}
|
||||
data-flx="invite.invite-accept-modal.render-body.button.accept"
|
||||
>
|
||||
{packActionLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={styles.cardInner} data-flx="invite.invite-accept-modal.render-body.card-inner--2">
|
||||
<InviteHeader invite={inviteForHeader} data-flx="invite.invite-accept-modal.render-body.invite-header--2" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as InviteCommands from '@app/features/invite/commands/InviteCommands';
|
||||
import {isGuildInvite, isPackInvite} from '@app/features/invite/types/InviteTypes';
|
||||
import {isGuildInvite} from '@app/features/invite/types/InviteTypes';
|
||||
import type {Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import {action, computed, makeAutoObservable, runInAction} from 'mobx';
|
||||
|
||||
@@ -268,13 +268,12 @@ class Invites {
|
||||
handleInviteCreate = action((invite: Invite): void => {
|
||||
const alive = this.filterAlive(invite);
|
||||
if (alive === null) return;
|
||||
if (!isPackInvite(alive)) {
|
||||
const channelId = alive.channel.id;
|
||||
const next = new Map(this.channelInviteCache);
|
||||
next.set(channelId, withInvite(this.channelInviteCache.get(channelId) ?? [], alive));
|
||||
this.channelInviteCache = next;
|
||||
this.channelFetchStatus = new Map(this.channelFetchStatus).set(channelId, 'success');
|
||||
}
|
||||
const channelId = alive.channel.id;
|
||||
this.channelInviteCache = new Map(this.channelInviteCache).set(
|
||||
channelId,
|
||||
withInvite(this.channelInviteCache.get(channelId) ?? [], alive),
|
||||
);
|
||||
this.channelFetchStatus = new Map(this.channelFetchStatus).set(channelId, 'success');
|
||||
if (isGuildInvite(alive)) {
|
||||
const guildId = alive.guild.id;
|
||||
const next = new Map(this.guildInviteCache);
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
import {InviteTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
|
||||
import type {GroupDmInvite, GuildInvite, Invite, PackInvite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import type {GroupDmInvite, GuildInvite, Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
|
||||
export type InviteTypeValue = ValueOf<typeof InviteTypes>;
|
||||
export type PackInviteType = typeof InviteTypes.EMOJI_PACK | typeof InviteTypes.STICKER_PACK;
|
||||
|
||||
export const isGuildInvite = (invite: Invite): invite is GuildInvite => invite.type === InviteTypes.GUILD;
|
||||
export const isGroupDmInvite = (invite: Invite): invite is GroupDmInvite => invite.type === InviteTypes.GROUP_DM;
|
||||
export const isPackInvite = (invite: Invite): invite is PackInvite =>
|
||||
invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK;
|
||||
@@ -5,7 +5,6 @@ import type {Channel} from '@app/features/channel/models/Channel';
|
||||
import Channels from '@app/features/channel/state/Channels';
|
||||
import {UNKNOWN_CHANNEL_DESCRIPTOR} from '@app/features/channel/utils/ChannelMessageDescriptors';
|
||||
import * as ChannelUtils from '@app/features/channel/utils/ChannelUtils';
|
||||
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
|
||||
import type {Guild} from '@app/features/guild/models/Guild';
|
||||
import Guilds from '@app/features/guild/state/Guilds';
|
||||
import {
|
||||
@@ -323,12 +322,7 @@ export function buildCandidateSets(i18n: I18n): CandidateSets {
|
||||
});
|
||||
}
|
||||
const settingsCandidates: Array<SettingsCandidate> = [];
|
||||
const hasExpressionPackAccess =
|
||||
(Users.getCurrentUser()?.isStaff() ?? false) && DeveloperOptions.showExpressionPacksSettings;
|
||||
const accessibleTabs = getSettingsTabs(i18n).filter((tab) => {
|
||||
if (!hasExpressionPackAccess && tab.type === 'expression_packs') {
|
||||
return false;
|
||||
}
|
||||
if (!UserSettings.developerMode && (tab.type === 'embed_debugger' || tab.type === 'component_gallery')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type SettingsTab,
|
||||
} from '@app/features/user/components/settings_utils/SettingsConstants';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
@@ -26,7 +25,6 @@ interface SettingsContextMenuProps {
|
||||
|
||||
export const SettingsContextMenu: React.FC<SettingsContextMenuProps> = observer(({onClose}) => {
|
||||
const {i18n} = useLingui();
|
||||
const hasExpressionPackAccess = Users.getCurrentUser()?.isStaff() ?? false;
|
||||
const handleOpenSettings = useCallback(
|
||||
(tab: SettingsTab, subtab?: SettingsSubtab) => {
|
||||
ModalCommands.push(
|
||||
@@ -92,15 +90,12 @@ export const SettingsContextMenu: React.FC<SettingsContextMenuProps> = observer(
|
||||
const accessibleTabs = useMemo(() => {
|
||||
const allTabs = getSettingsTabs(i18n);
|
||||
return allTabs.filter((tab) => {
|
||||
if (!hasExpressionPackAccess && tab.type === 'expression_packs') {
|
||||
return false;
|
||||
}
|
||||
if (!isDeveloperModeEnabled && (tab.type === 'embed_debugger' || tab.type === 'component_gallery')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [hasExpressionPackAccess, i18n.locale, isDeveloperModeEnabled]);
|
||||
}, [i18n.locale, isDeveloperModeEnabled]);
|
||||
const userSettingsTabs = accessibleTabs.filter((tab) => tab.category === 'user_settings');
|
||||
const billingTabs = accessibleTabs.filter((tab) => tab.category === 'billing');
|
||||
const appSettingsTabs = accessibleTabs.filter((tab) => tab.category === 'app_settings');
|
||||
|
||||
@@ -4,7 +4,6 @@ import {DesktopSettingsView} from '@app/features/app/components/dialogs/componen
|
||||
import {MobileSettingsView} from '@app/features/app/components/dialogs/components/MobileSettingsView';
|
||||
import * as Modal from '@app/features/app/components/dialogs/Modal';
|
||||
import {SettingsModalContainer} from '@app/features/app/components/dialogs/shared/SettingsModalLayout';
|
||||
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
|
||||
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import * as UnsavedChangesCommands from '@app/features/ui/commands/UnsavedChangesCommands';
|
||||
@@ -24,7 +23,6 @@ import type {UserSettingsTabType} from '@app/features/user/components/settings_u
|
||||
import {useMobileNavigation} from '@app/features/user/hooks/useMobileNavigation';
|
||||
import {SettingsContentKeyProvider} from '@app/features/user/hooks/useSettingsContentKey';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
@@ -45,20 +43,15 @@ export const UserSettingsModal: React.FC<UserSettingsModalProps> = observer(
|
||||
getAccountSectionForNestedTab(initialTab) ?? getAccountSectionForLegacySection(initialSubtab);
|
||||
const normalizedInitialTab = initialAccountSection ? ACCOUNT_SETTINGS_TAB : initialTab;
|
||||
const normalizedInitialSubtab = initialAccountSection ?? initialSubtab;
|
||||
const isStaff = Users.getCurrentUser()?.isStaff() ?? false;
|
||||
const hasExpressionPackAccess = isStaff && DeveloperOptions.showExpressionPacksSettings;
|
||||
const isDeveloperModeEnabled = UserSettings.developerMode;
|
||||
const visibleSettingsTabs = useMemo(() => {
|
||||
return settingsTabs.filter((tab) => {
|
||||
if (!hasExpressionPackAccess && tab.type === 'expression_packs') {
|
||||
return false;
|
||||
}
|
||||
if (!isDeveloperModeEnabled && (tab.type === 'embed_debugger' || tab.type === 'component_gallery')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [hasExpressionPackAccess, isDeveloperModeEnabled, settingsTabs]);
|
||||
}, [isDeveloperModeEnabled, settingsTabs]);
|
||||
const resolveVisibleTab = useCallback(
|
||||
(tabType?: UserSettingsTabType): UserSettingsTabType => {
|
||||
if (tabType && visibleSettingsTabs.some((tab) => tab.type === tabType)) {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.emptyState {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.spinnerWrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sectionActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--form-surface-background);
|
||||
border: 0.0625rem solid var(--background-modifier-selected);
|
||||
border-radius: var(--radius-xl);
|
||||
margin-bottom: var(--spacing-6);
|
||||
padding: var(--spacing-5);
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sectionSubtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.packCard {
|
||||
background: var(--background-secondary);
|
||||
border: 0.0625rem solid var(--background-modifier-selected);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.packCardHeader {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.packName {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.packMeta {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.packDescription {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 var(--spacing-3);
|
||||
}
|
||||
|
||||
.packTimestamp {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75rem;
|
||||
margin: 0 0 var(--spacing-3);
|
||||
}
|
||||
|
||||
.cardActions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
|
||||
import {SettingsSection} from '@app/features/app/components/dialogs/shared/SettingsSection';
|
||||
import {SettingsTabContainer, SettingsTabSection} from '@app/features/app/components/dialogs/shared/SettingsTabLayout';
|
||||
import {StatusSlate} from '@app/features/app/components/dialogs/shared/StatusSlate';
|
||||
import {PREMIUM_PRODUCT_FULL_NAME, PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter';
|
||||
import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils';
|
||||
import {CreatePackModal} from '@app/features/expressions/components/modals/CreatePackModal';
|
||||
import {EditPackModal} from '@app/features/expressions/components/modals/EditPackModal';
|
||||
import {PackInviteModal} from '@app/features/expressions/components/modals/PackInviteModal';
|
||||
import Packs from '@app/features/expressions/state/ExpressionsPacks';
|
||||
import {NO_DESCRIPTION_PROVIDED_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import {Spinner} from '@app/features/ui/components/Spinner';
|
||||
import styles from '@app/features/user/components/modals/tabs/ExpressionPacksTab.module.css';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {getFormattedShortDate} from '@app/features/user/utils/DateFormatting';
|
||||
import type {PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {StickerIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
|
||||
const UNLIMITED_DESCRIPTOR = msg({
|
||||
message: 'Unlimited',
|
||||
comment: 'Short label in the expression packs tab. Keep it concise.',
|
||||
});
|
||||
const UNABLE_TO_LOAD_PACK_INFORMATION_DESCRIPTOR = msg({
|
||||
message: 'Unable to load pack information.',
|
||||
comment: 'Error message in the expression packs tab.',
|
||||
});
|
||||
const DELETE_PACK_DESCRIPTOR = msg({
|
||||
message: 'Delete pack',
|
||||
comment:
|
||||
'Button or menu action label in the expression packs tab. Keep it concise. Keep the tone plain and specific.',
|
||||
});
|
||||
const ARE_YOU_SURE_YOU_WANT_TO_DELETE_THIS_DESCRIPTOR = msg({
|
||||
message: "Delete this pack? Can't be undone.",
|
||||
comment: 'Error message in the expression packs tab. Keep the tone plain and specific.',
|
||||
});
|
||||
const DELETE_DESCRIPTOR = msg({
|
||||
message: 'Delete',
|
||||
comment:
|
||||
'Button or menu action label in the expression packs tab. Keep it concise. Keep the tone plain and specific.',
|
||||
});
|
||||
const REMOVE_PACK_DESCRIPTOR = msg({
|
||||
message: 'Remove pack',
|
||||
comment:
|
||||
'Button or menu action label in the expression packs tab. Keep it concise. Keep the tone plain and specific.',
|
||||
});
|
||||
const EXPRESSION_PACKS_PREMIUM_FEATURE_DESCRIPTOR = msg({
|
||||
message: 'Expression packs are a {premiumProductName} feature',
|
||||
comment: 'Empty-state title for expression packs when the feature requires premium.',
|
||||
});
|
||||
const PREMIUM_EXPRESSION_PACKS_DESCRIPTION_DESCRIPTOR = msg({
|
||||
message: 'Create and share custom emoji and sticker packs with {premiumProductFullName}.',
|
||||
comment: 'Empty-state description for expression packs when the feature requires premium.',
|
||||
});
|
||||
const LEARN_ABOUT_PREMIUM_DESCRIPTOR = msg({
|
||||
message: 'Learn about {premiumProductName}',
|
||||
comment: 'CTA label that opens the premium settings tab from expression packs.',
|
||||
});
|
||||
const REMOVING_THE_PACK_WILL_UNINSTALL_IT_FROM_YOUR_DESCRIPTOR = msg({
|
||||
message: 'Removing the pack will uninstall it from your account.',
|
||||
comment: 'Description text in the expression packs tab.',
|
||||
});
|
||||
const REMOVE_DESCRIPTOR = msg({
|
||||
message: 'Remove',
|
||||
comment:
|
||||
'Button or menu action label in the expression packs tab. Keep it concise. Keep the tone plain and specific.',
|
||||
});
|
||||
const EMOJI_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Emoji packs',
|
||||
comment: 'Short label in the expression packs tab. Keep it concise.',
|
||||
});
|
||||
const STICKER_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Sticker packs',
|
||||
comment: 'Short label in the expression packs tab. Keep it concise.',
|
||||
});
|
||||
const PACK_TYPES: Array<{key: 'emoji' | 'sticker'}> = [{key: 'emoji'}, {key: 'sticker'}];
|
||||
const PackCard: React.FC<{
|
||||
pack: PackSummaryResponse;
|
||||
onUninstall?: () => void;
|
||||
onEdit?: () => void;
|
||||
onInvite?: () => void;
|
||||
created?: boolean;
|
||||
}> = observer(({pack, onUninstall, onEdit, onInvite, created}) => {
|
||||
const {i18n} = useLingui();
|
||||
const installedAt = pack.installed_at ? getFormattedShortDate(new Date(pack.installed_at)) : null;
|
||||
return (
|
||||
<div className={styles.packCard} data-flx="user.expression-packs-tab.pack-card.pack-card">
|
||||
<div className={styles.packCardHeader} data-flx="user.expression-packs-tab.pack-card.pack-card-header">
|
||||
<h3 className={styles.packName} data-flx="user.expression-packs-tab.pack-card.pack-name">
|
||||
{pack.name}
|
||||
</h3>
|
||||
<span className={styles.packMeta} data-flx="user.expression-packs-tab.pack-card.pack-meta">
|
||||
{pack.type === 'emoji' ? <Trans>Emoji pack</Trans> : <Trans>Sticker pack</Trans>}
|
||||
</span>
|
||||
</div>
|
||||
<p className={styles.packDescription} data-flx="user.expression-packs-tab.pack-card.pack-description">
|
||||
{pack.description || i18n._(NO_DESCRIPTION_PROVIDED_DESCRIPTOR)}
|
||||
</p>
|
||||
{installedAt && (
|
||||
<p className={styles.packTimestamp} data-flx="user.expression-packs-tab.pack-card.pack-timestamp">
|
||||
<Trans>Installed on {installedAt}</Trans>
|
||||
</p>
|
||||
)}
|
||||
<div className={styles.cardActions} data-flx="user.expression-packs-tab.pack-card.card-actions">
|
||||
{created && (
|
||||
<>
|
||||
<Button variant="secondary" onClick={onInvite} data-flx="user.expression-packs-tab.pack-card.button.invite">
|
||||
<Trans>Invite</Trans>
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onEdit} data-flx="user.expression-packs-tab.pack-card.button.edit">
|
||||
<Trans>Edit</Trans>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{onUninstall && (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={onUninstall}
|
||||
data-flx="user.expression-packs-tab.pack-card.button.uninstall"
|
||||
>
|
||||
<Trans>Remove</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const ExpressionPacksTab: React.FC = observer(() => {
|
||||
const {i18n} = useLingui();
|
||||
const formatLimit = (value: number): string => {
|
||||
if (value === Number.POSITIVE_INFINITY) return i18n._(UNLIMITED_DESCRIPTOR);
|
||||
return value.toString();
|
||||
};
|
||||
const currentUser = Users.currentUser;
|
||||
const hasGlobalExpressions = useMemo(
|
||||
() =>
|
||||
isLimitToggleEnabled(
|
||||
{feature_global_expressions: LimitResolver.resolve({key: 'feature_global_expressions', fallback: 0})},
|
||||
'feature_global_expressions',
|
||||
),
|
||||
[],
|
||||
);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!hasGlobalExpressions || loaded) return;
|
||||
Packs.fetch().finally(() => setLoaded(true));
|
||||
}, [hasGlobalExpressions, loaded]);
|
||||
if (!currentUser) return null;
|
||||
if (!hasGlobalExpressions && RuntimeConfig.isSelfHosted()) {
|
||||
return (
|
||||
<div className={styles.emptyState} data-flx="user.expression-packs-tab.empty-state">
|
||||
<StatusSlate
|
||||
Icon={StickerIcon}
|
||||
title={<Trans>Expression packs</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
Expression packs are not enabled on this instance. Contact your instance administrator for more
|
||||
information.
|
||||
</Trans>
|
||||
}
|
||||
data-flx="user.expression-packs-tab.status-slate"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!hasGlobalExpressions) {
|
||||
return (
|
||||
<div className={styles.emptyState} data-flx="user.expression-packs-tab.empty-state--2">
|
||||
<StatusSlate
|
||||
Icon={StickerIcon}
|
||||
title={i18n._(EXPRESSION_PACKS_PREMIUM_FEATURE_DESCRIPTOR, {
|
||||
premiumProductName: PREMIUM_PRODUCT_NAME,
|
||||
})}
|
||||
description={i18n._(PREMIUM_EXPRESSION_PACKS_DESCRIPTION_DESCRIPTOR, {
|
||||
premiumProductFullName: PREMIUM_PRODUCT_FULL_NAME,
|
||||
})}
|
||||
actions={[
|
||||
{
|
||||
text: i18n._(LEARN_ABOUT_PREMIUM_DESCRIPTOR, {premiumProductName: PREMIUM_PRODUCT_NAME}),
|
||||
onClick: () => ComponentBus.dispatch('USER_SETTINGS_TAB_SELECT', {tab: 'plutonium'}),
|
||||
variant: 'primary',
|
||||
fitContent: true,
|
||||
},
|
||||
]}
|
||||
data-flx="user.expression-packs-tab.status-slate--2"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const dashboard = Packs.dashboard;
|
||||
const fetchStatus = Packs.fetchStatus;
|
||||
if (fetchStatus === 'pending') {
|
||||
return (
|
||||
<div className={styles.spinnerWrapper} data-flx="user.expression-packs-tab.spinner-wrapper">
|
||||
<Spinner data-flx="user.expression-packs-tab.spinner" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!dashboard) {
|
||||
return (
|
||||
<div className={styles.emptyState} data-flx="user.expression-packs-tab.empty-state--3">
|
||||
<p data-flx="user.expression-packs-tab.p">{i18n._(UNABLE_TO_LOAD_PACK_INFORMATION_DESCRIPTOR)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const handleOpenCreate = (type: 'emoji' | 'sticker') => {
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<CreatePackModal
|
||||
type={type}
|
||||
onSuccess={() => Packs.fetch()}
|
||||
data-flx="user.expression-packs-tab.handle-open-create.create-pack-modal"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
const handleOpenEdit = (pack: PackSummaryResponse) => {
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<EditPackModal
|
||||
packId={pack.id}
|
||||
type={pack.type}
|
||||
name={pack.name}
|
||||
description={pack.description}
|
||||
onSuccess={() => Packs.fetch()}
|
||||
data-flx="user.expression-packs-tab.handle-open-edit.edit-pack-modal"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
const handleOpenInvite = (pack: PackSummaryResponse) => {
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<PackInviteModal
|
||||
packId={pack.id}
|
||||
type={pack.type}
|
||||
onCreated={() => Packs.fetch()}
|
||||
data-flx="user.expression-packs-tab.handle-open-invite.pack-invite-modal"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
const handleDelete = (packId: string) => {
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={i18n._(DELETE_PACK_DESCRIPTOR)}
|
||||
description={i18n._(ARE_YOU_SURE_YOU_WANT_TO_DELETE_THIS_DESCRIPTOR)}
|
||||
primaryText={i18n._(DELETE_DESCRIPTOR)}
|
||||
primaryVariant="danger"
|
||||
onPrimary={async () => {
|
||||
await Packs.deletePack(packId);
|
||||
}}
|
||||
data-flx="user.expression-packs-tab.handle-delete.confirm-modal"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
const handleUninstall = (packId: string) => {
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={i18n._(REMOVE_PACK_DESCRIPTOR)}
|
||||
description={i18n._(REMOVING_THE_PACK_WILL_UNINSTALL_IT_FROM_YOUR_DESCRIPTOR)}
|
||||
primaryText={i18n._(REMOVE_DESCRIPTOR)}
|
||||
onPrimary={async () => {
|
||||
await Packs.uninstallPack(packId);
|
||||
}}
|
||||
data-flx="user.expression-packs-tab.handle-uninstall.confirm-modal"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<SettingsTabContainer data-flx="user.expression-packs-tab.settings-tab-container">
|
||||
{PACK_TYPES.map((section) => {
|
||||
const data = section.key === 'emoji' ? dashboard.emoji : dashboard.sticker;
|
||||
const sectionId = section.key === 'emoji' ? 'emoji-packs' : 'sticker-packs';
|
||||
const sectionLabel =
|
||||
section.key === 'emoji' ? i18n._(EMOJI_PACKS_DESCRIPTOR) : i18n._(STICKER_PACKS_DESCRIPTOR);
|
||||
const installedCount = data.installed.length;
|
||||
const installedLimit = formatLimit(data.installed_limit);
|
||||
const createdCount = data.created.length;
|
||||
const createdLimit = formatLimit(data.created_limit);
|
||||
return (
|
||||
<SettingsSection
|
||||
key={section.key}
|
||||
id={sectionId}
|
||||
title={sectionLabel}
|
||||
description={
|
||||
<Trans>
|
||||
Installed {installedCount} / {installedLimit}
|
||||
</Trans>
|
||||
}
|
||||
data-flx="user.expression-packs-tab.section"
|
||||
>
|
||||
<div className={styles.sectionActions} data-flx="user.expression-packs-tab.section-actions">
|
||||
<Button
|
||||
onClick={() => handleOpenCreate(section.key)}
|
||||
data-flx="user.expression-packs-tab.button.open-create"
|
||||
>
|
||||
{section.key === 'emoji' ? <Trans>Create emoji pack</Trans> : <Trans>Create sticker pack</Trans>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.listWrapper} data-flx="user.expression-packs-tab.list-wrapper">
|
||||
{data.installed.length === 0 && (
|
||||
<p className={styles.emptyText} data-flx="user.expression-packs-tab.empty-text">
|
||||
<Trans>No installed packs yet.</Trans>
|
||||
</p>
|
||||
)}
|
||||
{data.installed.map((pack) => (
|
||||
<PackCard
|
||||
key={pack.id}
|
||||
pack={pack}
|
||||
onUninstall={() => handleUninstall(pack.id)}
|
||||
data-flx="user.expression-packs-tab.pack-card"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<SettingsTabSection
|
||||
title={
|
||||
<Trans>
|
||||
Created {createdCount} / {createdLimit}
|
||||
</Trans>
|
||||
}
|
||||
data-flx="user.expression-packs-tab.created-section"
|
||||
>
|
||||
<div className={styles.listWrapper} data-flx="user.expression-packs-tab.list-wrapper--2">
|
||||
{data.created.length === 0 && (
|
||||
<p className={styles.emptyText} data-flx="user.expression-packs-tab.empty-text--2">
|
||||
<Trans>You haven't created any packs yet.</Trans>
|
||||
</p>
|
||||
)}
|
||||
{data.created.map((pack) => (
|
||||
<PackCard
|
||||
key={pack.id}
|
||||
pack={pack}
|
||||
created={true}
|
||||
onInvite={() => handleOpenInvite(pack)}
|
||||
onEdit={() => handleOpenEdit(pack)}
|
||||
onUninstall={() => handleDelete(pack.id)}
|
||||
data-flx="user.expression-packs-tab.pack-card--2"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SettingsTabSection>
|
||||
</SettingsSection>
|
||||
);
|
||||
})}
|
||||
</SettingsTabContainer>
|
||||
);
|
||||
});
|
||||
|
||||
export default ExpressionPacksTab;
|
||||
-1
@@ -52,7 +52,6 @@ const SOURCE_TAB_CATEGORY: Partial<Record<UserSettingsTabType, SettingsCategoryT
|
||||
linked_accounts: 'account',
|
||||
plutonium: 'account',
|
||||
gift_inventory: 'account',
|
||||
expression_packs: 'account',
|
||||
privacy_safety: 'privacy',
|
||||
authorized_apps: 'privacy',
|
||||
blocked_users: 'privacy',
|
||||
|
||||
@@ -9,7 +9,6 @@ import ChatSettingsTab from '@app/features/user/components/modals/tabs/ChatSetti
|
||||
import ComponentGalleryTab from '@app/features/user/components/modals/tabs/component_gallery_tab';
|
||||
import DesktopSettingsTab from '@app/features/user/components/modals/tabs/DesktopSettingsTab';
|
||||
import EmbedDebuggerTab from '@app/features/user/components/modals/tabs/EmbedDebuggerTab';
|
||||
import ExpressionPacksTab from '@app/features/user/components/modals/tabs/ExpressionPacksTab';
|
||||
import GiftInventoryTab from '@app/features/user/components/modals/tabs/GiftInventoryTab';
|
||||
import KeybindsTab from '@app/features/user/components/modals/tabs/KeybindsTab';
|
||||
import LanguageTab from '@app/features/user/components/modals/tabs/LanguageTab';
|
||||
@@ -44,7 +43,6 @@ const DESKTOP_TAB_COMPONENTS: Partial<Record<UserSettingsTabType, React.Componen
|
||||
embed_debugger: EmbedDebuggerTab,
|
||||
applications: ApplicationsTab,
|
||||
component_gallery: ComponentGalleryTab,
|
||||
expression_packs: ExpressionPacksTab,
|
||||
};
|
||||
export const getSettingsTabComponent = (
|
||||
tabType: UserSettingsTabType,
|
||||
|
||||
@@ -44,17 +44,12 @@ import {
|
||||
ProhibitIcon,
|
||||
RobotIcon,
|
||||
ShieldIcon,
|
||||
StickerIcon,
|
||||
TranslateIcon,
|
||||
UserIcon,
|
||||
UserListIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import type React from 'react';
|
||||
|
||||
const EXPRESSION_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Expression packs',
|
||||
comment: 'User settings tab for purchased or owned emoji/sticker expression packs.',
|
||||
});
|
||||
const BLOCKED_USERS_DESCRIPTOR = msg({
|
||||
message: 'Blocked users',
|
||||
comment: 'User settings tab listing accounts the current user has blocked.',
|
||||
@@ -267,12 +262,6 @@ const ALL_TABS_DESCRIPTORS: Array<SettingsTabDescriptor> = [
|
||||
label: GIFTS_AND_CODES_DESCRIPTOR,
|
||||
icon: GiftIcon,
|
||||
},
|
||||
{
|
||||
type: 'expression_packs',
|
||||
category: 'billing',
|
||||
label: EXPRESSION_PACKS_DESCRIPTOR,
|
||||
icon: StickerIcon,
|
||||
},
|
||||
{
|
||||
type: 'appearance',
|
||||
category: 'app_settings',
|
||||
|
||||
@@ -31,7 +31,6 @@ import {clientDeveloperSettingsIndex} from '@app/features/user/components/settin
|
||||
import {desktopSettingsIndex} from '@app/features/user/components/settings_utils/search_index/DesktopSettingsIndex';
|
||||
import {devicesIndex} from '@app/features/user/components/settings_utils/search_index/DevicesIndex';
|
||||
import {embedDebuggerIndex} from '@app/features/user/components/settings_utils/search_index/EmbedDebuggerIndex';
|
||||
import {expressionPacksIndex} from '@app/features/user/components/settings_utils/search_index/ExpressionPacksIndex';
|
||||
import {giftInventoryIndex} from '@app/features/user/components/settings_utils/search_index/GiftInventoryIndex';
|
||||
import {keybindsIndex} from '@app/features/user/components/settings_utils/search_index/KeybindsIndex';
|
||||
import {languageIndex} from '@app/features/user/components/settings_utils/search_index/LanguageIndex';
|
||||
@@ -100,7 +99,6 @@ const ADDITIONAL_SEARCHABLE_ITEMS: Array<SearchableSettingDescriptor> = [
|
||||
...devicesIndex,
|
||||
...plutoniumIndex,
|
||||
...giftInventoryIndex,
|
||||
...expressionPacksIndex,
|
||||
...appearanceIndex,
|
||||
...notificationsIndex,
|
||||
...chatSettingsIndex,
|
||||
|
||||
@@ -38,7 +38,6 @@ const USER_SETTINGS_TAB_TYPES = new Set<UserSettingsTabType>([
|
||||
'component_gallery',
|
||||
'language',
|
||||
'keybinds',
|
||||
'expression_packs',
|
||||
'linked_accounts',
|
||||
]);
|
||||
const SAFE_SETTINGS_PARAM_REGEX = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {STICKERS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import type {SearchableSettingDescriptor} from '@app/features/user/components/settings_utils/search_index/SearchIndexTypes';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
const EXPRESSION_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Expression packs',
|
||||
comment: 'Settings search entry label. Names the settings search entry in the settings UI.',
|
||||
});
|
||||
const EMOJI_DESCRIPTOR = msg({
|
||||
message: 'Emoji',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const PACKS_DESCRIPTOR = msg({
|
||||
message: 'Packs',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const EXPRESSIONS_DESCRIPTOR = msg({
|
||||
message: 'Expressions',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const EMOJI_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Emoji packs',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const STICKER_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Sticker packs',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const CREATE_EMOJI_PACK_DESCRIPTOR = msg({
|
||||
message: 'Create emoji pack',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const CREATE_STICKER_PACK_DESCRIPTOR = msg({
|
||||
message: 'Create sticker pack',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const INSTALLED_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Installed packs',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const CREATED_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Created packs',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const MANAGE_EXPRESSION_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Manage expression packs',
|
||||
comment: 'Settings search entry description. One-line summary of what the settings search entry controls.',
|
||||
});
|
||||
export const expressionPacksIndex: Array<SearchableSettingDescriptor> = [
|
||||
{
|
||||
id: 'expression-packs',
|
||||
tabType: 'expression_packs',
|
||||
label: EXPRESSION_PACKS_DESCRIPTOR,
|
||||
keywords: [
|
||||
STICKERS_DESCRIPTOR,
|
||||
EMOJI_DESCRIPTOR,
|
||||
PACKS_DESCRIPTOR,
|
||||
EXPRESSIONS_DESCRIPTOR,
|
||||
EMOJI_PACKS_DESCRIPTOR,
|
||||
STICKER_PACKS_DESCRIPTOR,
|
||||
CREATE_EMOJI_PACK_DESCRIPTOR,
|
||||
CREATE_STICKER_PACK_DESCRIPTOR,
|
||||
INSTALLED_PACKS_DESCRIPTOR,
|
||||
CREATED_PACKS_DESCRIPTOR,
|
||||
],
|
||||
description: MANAGE_EXPRESSION_PACKS_DESCRIPTOR,
|
||||
},
|
||||
];
|
||||
-10
@@ -15,14 +15,6 @@ const PURCHASED_GIFTS_DESCRIPTOR = msg({
|
||||
message: 'Purchased gifts',
|
||||
comment: 'Settings section label for managing purchased gift codes.',
|
||||
});
|
||||
const EMOJI_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Emoji packs',
|
||||
comment: 'Settings section label for emoji expression packs.',
|
||||
});
|
||||
const STICKER_PACKS_DESCRIPTOR = msg({
|
||||
message: 'Sticker packs',
|
||||
comment: 'Settings section label for sticker expression packs.',
|
||||
});
|
||||
const BLOCKED_USERS_DESCRIPTOR = msg({
|
||||
message: 'Blocked users',
|
||||
comment: 'Settings section label for blocked users.',
|
||||
@@ -92,8 +84,6 @@ export const generalSettingsSections = [
|
||||
keywords: [],
|
||||
isAdvanced: false,
|
||||
},
|
||||
{id: 'emoji-packs', tabType: 'expression_packs', label: EMOJI_PACKS_DESCRIPTOR, keywords: [], isAdvanced: false},
|
||||
{id: 'sticker-packs', tabType: 'expression_packs', label: STICKER_PACKS_DESCRIPTOR, keywords: [], isAdvanced: false},
|
||||
{id: 'blocked-users', tabType: 'blocked_users', label: BLOCKED_USERS_DESCRIPTOR, keywords: [], isAdvanced: false},
|
||||
{
|
||||
id: 'authorized-applications',
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ export type UserSettingsTabType =
|
||||
| 'component_gallery'
|
||||
| 'language'
|
||||
| 'keybinds'
|
||||
| 'expression_packs'
|
||||
| 'linked_accounts';
|
||||
|
||||
export interface SectionDefinition extends SettingsMetadata {
|
||||
|
||||
Reference in new issue
Block a user