feat(expressions): remove the unfinished packs feature (#2250)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-31 16:34:22 +02:00
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 {
@@ -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;
@@ -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_-]+$/;
@@ -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,
},
];
@@ -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',
@@ -32,7 +32,6 @@ export type UserSettingsTabType =
| 'component_gallery'
| 'language'
| 'keybinds'
| 'expression_packs'
| 'linked_accounts';
export interface SectionDefinition extends SettingsMetadata {