mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-02 21:04:06 +03:00
feat(expressions): remove the unfinished packs feature (#2250)
This commit is contained in:
@@ -8959,7 +8959,6 @@
|
||||
"INVALID_FORM_BODY",
|
||||
"INVALID_GRANT",
|
||||
"INVALID_HANDOFF_CODE",
|
||||
"INVALID_PACK_TYPE",
|
||||
"INVALID_PERMISSIONS_INTEGER",
|
||||
"INVALID_PERMISSIONS_NEGATIVE",
|
||||
"INVALID_PHONE_NUMBER",
|
||||
@@ -9002,8 +9001,6 @@
|
||||
"MAX_GUILDS",
|
||||
"NEW_ACCOUNT_GUILD_JOIN_RATE_LIMITED",
|
||||
"MAX_INVITES",
|
||||
"MAX_PACK_EXPRESSIONS",
|
||||
"MAX_PACKS",
|
||||
"MAX_PINS_PER_CHANNEL",
|
||||
"MESSAGE_TOTAL_ATTACHMENT_SIZE_TOO_LARGE",
|
||||
"MAX_REACTIONS",
|
||||
@@ -9038,7 +9035,6 @@
|
||||
"NOT_OWNER_OF_ADMIN_API_KEY",
|
||||
"NSFW_CONTENT_AGE_RESTRICTED",
|
||||
"NSFW_EMOJI_STICKER_BLOCKED",
|
||||
"PACK_ACCESS_DENIED",
|
||||
"PASSKEY_AUTHENTICATION_FAILED",
|
||||
"PASSKEYS_DISABLED",
|
||||
"PHONE_ADD_NOT_ELIGIBLE",
|
||||
@@ -9117,7 +9113,6 @@
|
||||
"UNKNOWN_INVITE",
|
||||
"UNKNOWN_MEMBER",
|
||||
"UNKNOWN_MESSAGE",
|
||||
"UNKNOWN_PACK",
|
||||
"UNKNOWN_REPORT",
|
||||
"UNKNOWN_ROLE",
|
||||
"UNKNOWN_STICKER",
|
||||
|
||||
@@ -10,7 +10,6 @@ import {IntegrationRateLimitConfigs} from './rate_limit_configs/IntegrationRateL
|
||||
import {InviteRateLimitConfigs} from './rate_limit_configs/InviteRateLimitConfig';
|
||||
import {MiscRateLimitConfigs} from './rate_limit_configs/MiscRateLimitConfig';
|
||||
import {OAuthRateLimitConfigs} from './rate_limit_configs/OAuthRateLimitConfig';
|
||||
import {PackRateLimitConfigs} from './rate_limit_configs/PackRateLimitConfig';
|
||||
import type {RateLimitSection} from './rate_limit_configs/RateLimitHelpers';
|
||||
import {mergeRateLimitSections} from './rate_limit_configs/RateLimitHelpers';
|
||||
import {UserRateLimitConfigs} from './rate_limit_configs/UserRateLimitConfig';
|
||||
@@ -29,6 +28,5 @@ const rateLimitSections = [
|
||||
IntegrationRateLimitConfigs,
|
||||
AdminRateLimitConfigs,
|
||||
MiscRateLimitConfigs,
|
||||
PackRateLimitConfigs,
|
||||
] satisfies ReadonlyArray<RateLimitSection>;
|
||||
export const RateLimitConfigs = mergeRateLimitSections(...rateLimitSections);
|
||||
|
||||
@@ -290,13 +290,10 @@ import {
|
||||
type SuspiciousIpRow,
|
||||
} from './database/types/RiskTypes';
|
||||
import {
|
||||
EXPRESSION_PACK_COLUMNS,
|
||||
type ExpressionPackRow,
|
||||
FAVORITE_MEME_COLUMNS,
|
||||
type FavoriteMemeRow,
|
||||
NOTE_COLUMNS,
|
||||
type NoteRow,
|
||||
type PackInstallationRow,
|
||||
PUSH_SUBSCRIPTION_COLUMNS,
|
||||
type PushSubscriptionRow,
|
||||
RECENT_MENTION_COLUMNS,
|
||||
@@ -1004,25 +1001,6 @@ export const FavoriteMemesByMemeId = defineTable<FavoriteMemesByMemeIdRow, 'meme
|
||||
columns: FAVORITE_MEMES_BY_MEME_ID_COLUMNS,
|
||||
primaryKey: ['meme_id', 'user_id'],
|
||||
});
|
||||
export const ExpressionPacks = defineTable<ExpressionPackRow, 'pack_id'>({
|
||||
name: 'expression_packs',
|
||||
columns: EXPRESSION_PACK_COLUMNS,
|
||||
primaryKey: ['pack_id'],
|
||||
});
|
||||
export const ExpressionPacksByCreator = defineTable<ExpressionPackRow, 'creator_id' | 'pack_id'>({
|
||||
name: 'expression_packs_by_creator',
|
||||
columns: EXPRESSION_PACK_COLUMNS,
|
||||
primaryKey: ['creator_id', 'pack_id'],
|
||||
partitionKey: ['creator_id'],
|
||||
});
|
||||
const PACK_INSTALLATION_COLUMNS = ['user_id', 'pack_id', 'pack_type', 'installed_at'] as const satisfies ReadonlyArray<
|
||||
keyof PackInstallationRow
|
||||
>;
|
||||
export const PackInstallations = defineTable<PackInstallationRow, 'user_id' | 'pack_id'>({
|
||||
name: 'pack_installations',
|
||||
columns: PACK_INSTALLATION_COLUMNS,
|
||||
primaryKey: ['user_id', 'pack_id'],
|
||||
});
|
||||
|
||||
interface InvitesByChannelRow {
|
||||
channel_id: ChannelID;
|
||||
|
||||
@@ -24,7 +24,6 @@ import {getCacheService} from '../middleware/ServiceSingletons';
|
||||
import {OAuth2ApplicationsController} from '../oauth/OAuth2ApplicationsController';
|
||||
import {OAuth2Controller} from '../oauth/OAuth2Controller';
|
||||
import {OpenAPIController} from '../openapi/OpenAPIController';
|
||||
import {registerPackControllers} from '../pack/controllers/index';
|
||||
import {PremiumController} from '../premium/PremiumController';
|
||||
import {ReadStateController} from '../read_state/ReadStateController';
|
||||
import {ReportController} from '../report/ReportController';
|
||||
@@ -54,7 +53,6 @@ export function registerControllers(routes: HonoApp, config: APIConfig): void {
|
||||
FavoriteGifController(routes);
|
||||
FavoriteMemeController(routes);
|
||||
InviteController(routes);
|
||||
registerPackControllers(routes);
|
||||
ReadStateController(routes);
|
||||
ReportController(routes);
|
||||
GuildController(routes);
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {UserCacheService} from '../../infrastructure/UserCacheService';
|
||||
import type {IInviteRepository} from '../../invite/IInviteRepository';
|
||||
import type {LimitConfigService} from '../../limits/LimitConfigService';
|
||||
import type {User} from '../../models/User';
|
||||
import type {PackService} from '../../pack/PackService';
|
||||
import type {ReadStateService} from '../../read_state/ReadStateService';
|
||||
import type {IUserRepository} from '../../user/IUserRepository';
|
||||
import {createDirectMessageSpamMitigationService} from '../../user/services/DirectMessageSpamMitigationService';
|
||||
@@ -57,7 +56,6 @@ export class ChannelService {
|
||||
channelRepository: IChannelRepository,
|
||||
userRepository: IUserRepository,
|
||||
guildRepository: IGuildRepositoryAggregate,
|
||||
packService: PackService,
|
||||
userCacheService: UserCacheService,
|
||||
embedService: EmbedService,
|
||||
readStateService: ReadStateService,
|
||||
@@ -94,7 +92,6 @@ export class ChannelService {
|
||||
channelRepository,
|
||||
userRepository,
|
||||
guildRepository,
|
||||
packService,
|
||||
embedService,
|
||||
storageService,
|
||||
attachmentUploadTraceRepository,
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {GuildID, UserID, WebhookID} from '../../../BrandedTypes';
|
||||
import type {IGuildRepositoryAggregate} from '../../../guild/repositories/IGuildRepositoryAggregate';
|
||||
import type {LimitConfigService} from '../../../limits/LimitConfigService';
|
||||
import type {Channel} from '../../../models/Channel';
|
||||
import type {PackService} from '../../../pack/PackService';
|
||||
import type {IUserRepository} from '../../../user/IUserRepository';
|
||||
import * as EmojiUtils from '../../../utils/EmojiUtils';
|
||||
|
||||
@@ -22,7 +21,6 @@ export class MessageContentService {
|
||||
constructor(
|
||||
private userRepository: IUserRepository,
|
||||
private guildRepository: IGuildRepositoryAggregate,
|
||||
private packService: PackService,
|
||||
private limitConfigService: LimitConfigService,
|
||||
) {}
|
||||
|
||||
@@ -33,15 +31,10 @@ export class MessageContentService {
|
||||
guildId: GuildID | null;
|
||||
hasPermission?: (permission: bigint) => Promise<boolean>;
|
||||
}): Promise<string> {
|
||||
const packResolver = await this.packService.createPackExpressionAccessResolver({
|
||||
userId: params.userId,
|
||||
type: 'emoji',
|
||||
});
|
||||
return await EmojiUtils.sanitizeCustomEmojis({
|
||||
...params,
|
||||
userRepository: this.userRepository,
|
||||
guildRepository: this.guildRepository,
|
||||
packResolver,
|
||||
limitConfigService: this.limitConfigService,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import type {Channel} from '../../../models/Channel';
|
||||
import type {Message} from '../../../models/Message';
|
||||
import type {MessageSnapshot} from '../../../models/MessageSnapshot';
|
||||
import type {User} from '../../../models/User';
|
||||
import type {PackService} from '../../../pack/PackService';
|
||||
import type {ReadStateService} from '../../../read_state/ReadStateService';
|
||||
import type {IUserRepository} from '../../../user/IUserRepository';
|
||||
import {hasVisibleContent} from '../../../utils/StringUtils';
|
||||
@@ -119,7 +118,6 @@ export class MessagePersistenceService {
|
||||
private channelRepository: IChannelRepositoryAggregate,
|
||||
private userRepository: IUserRepository,
|
||||
private guildRepository: IGuildRepositoryAggregate,
|
||||
private packService: PackService,
|
||||
private embedService: EmbedService,
|
||||
storageService: IStorageService,
|
||||
attachmentUploadTraceRepository: AttachmentUploadTraceRepository,
|
||||
@@ -136,18 +134,8 @@ export class MessagePersistenceService {
|
||||
virusScanService,
|
||||
snowflakeService,
|
||||
);
|
||||
this.contentService = new MessageContentService(
|
||||
this.userRepository,
|
||||
guildRepository,
|
||||
this.packService,
|
||||
limitConfigService,
|
||||
);
|
||||
this.stickerService = new MessageStickerService(
|
||||
this.userRepository,
|
||||
guildRepository,
|
||||
this.packService,
|
||||
limitConfigService,
|
||||
);
|
||||
this.contentService = new MessageContentService(this.userRepository, guildRepository, limitConfigService);
|
||||
this.stickerService = new MessageStickerService(this.userRepository, guildRepository, limitConfigService);
|
||||
this.embedAttachmentResolver = new MessageEmbedAttachmentResolver();
|
||||
this.attachmentDecayService = new AttachmentDecayService();
|
||||
}
|
||||
|
||||
@@ -11,14 +11,12 @@ import type {IGuildRepositoryAggregate} from '../../../guild/repositories/IGuild
|
||||
import type {LimitConfigService} from '../../../limits/LimitConfigService';
|
||||
import {resolveLimitSafe} from '../../../limits/LimitConfigUtils';
|
||||
import {createLimitMatchContext} from '../../../limits/LimitMatchContextBuilder';
|
||||
import type {PackService} from '../../../pack/PackService';
|
||||
import type {IUserRepository} from '../../../user/IUserRepository';
|
||||
|
||||
export class MessageStickerService {
|
||||
constructor(
|
||||
private userRepository: IUserRepository,
|
||||
private guildRepository: IGuildRepositoryAggregate,
|
||||
private packService: PackService,
|
||||
private readonly limitConfigService: LimitConfigService,
|
||||
) {}
|
||||
|
||||
@@ -30,10 +28,6 @@ export class MessageStickerService {
|
||||
isNSFWAllowed?: boolean;
|
||||
}): Promise<Array<MessageStickerItem>> {
|
||||
const {stickerIds, userId, guildId, hasPermission, isNSFWAllowed = true} = params;
|
||||
const packResolver = await this.packService.createPackExpressionAccessResolver({
|
||||
userId,
|
||||
type: 'sticker',
|
||||
});
|
||||
let hasGlobalExpressions = 0;
|
||||
if (userId) {
|
||||
const user = await this.userRepository.findUnique(userId);
|
||||
@@ -55,10 +49,6 @@ export class MessageStickerService {
|
||||
if (!stickerFromAnyGuild) {
|
||||
throw InputValidationError.fromCode('sticker', ValidationErrorCodes.CUSTOM_STICKER_NOT_FOUND);
|
||||
}
|
||||
const packAccess = await packResolver.resolve(stickerFromAnyGuild.guildId);
|
||||
if (packAccess === 'not-accessible') {
|
||||
throw InputValidationError.fromCode('sticker', ValidationErrorCodes.CUSTOM_STICKER_NOT_FOUND);
|
||||
}
|
||||
if (!isNSFWAllowed && stickerFromAnyGuild.isNsfw) {
|
||||
throw new NsfwEmojiStickerBlockedError();
|
||||
}
|
||||
@@ -97,10 +87,6 @@ export class MessageStickerService {
|
||||
throw new MissingPermissionsError();
|
||||
}
|
||||
}
|
||||
const packAccess = await packResolver.resolve(stickerFromOtherGuild.guildId);
|
||||
if (packAccess === 'not-accessible') {
|
||||
throw InputValidationError.fromCode('sticker', ValidationErrorCodes.CUSTOM_STICKER_NOT_FOUND);
|
||||
}
|
||||
if (!isNSFWAllowed && stickerFromOtherGuild.isNsfw) {
|
||||
throw new NsfwEmojiStickerBlockedError();
|
||||
}
|
||||
|
||||
@@ -311,24 +311,6 @@ export interface UserGuildSettingsRow {
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface ExpressionPackRow {
|
||||
pack_id: GuildID;
|
||||
pack_type: string;
|
||||
creator_id: UserID;
|
||||
name: string;
|
||||
description: Nullish<string>;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface PackInstallationRow {
|
||||
user_id: UserID;
|
||||
pack_id: GuildID;
|
||||
pack_type: string;
|
||||
installed_at: Date;
|
||||
}
|
||||
|
||||
export interface SavedMessageRow {
|
||||
user_id: UserID;
|
||||
channel_id: ChannelID;
|
||||
@@ -515,16 +497,6 @@ export const USER_SETTINGS_COLUMNS = [
|
||||
'default_share_voice_activity',
|
||||
'version',
|
||||
] as const satisfies ReadonlyArray<keyof UserSettingsRow>;
|
||||
export const EXPRESSION_PACK_COLUMNS = [
|
||||
'pack_id',
|
||||
'pack_type',
|
||||
'creator_id',
|
||||
'name',
|
||||
'description',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'version',
|
||||
] as const satisfies ReadonlyArray<keyof ExpressionPackRow>;
|
||||
export const USER_GUILD_SETTINGS_COLUMNS = [
|
||||
'user_id',
|
||||
'guild_id',
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
ChannelIdParam,
|
||||
GuildIdParam,
|
||||
InviteCodeParam,
|
||||
PackIdParam,
|
||||
} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {ChannelIdParam, GuildIdParam, InviteCodeParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {
|
||||
ChannelInviteCreateRequest,
|
||||
InviteMetadataResponseSchema,
|
||||
InviteResponseSchema,
|
||||
PackInviteCreateRequest,
|
||||
} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import {z} from 'zod';
|
||||
import {createChannelID, createGuildID, createInviteCode} from '../BrandedTypes';
|
||||
@@ -30,7 +24,7 @@ export function InviteController(app: HonoApp) {
|
||||
operationId: 'get_invite',
|
||||
summary: 'Get invite information',
|
||||
description:
|
||||
'Fetches detailed information about an invite using its code, including the guild, channel, or pack it belongs to and metadata such as expiration and usage limits. This endpoint does not require authentication and does not consume the invite.',
|
||||
'Fetches detailed information about an invite using its code, including the guild or channel it belongs to and metadata such as expiration and usage limits. This endpoint does not require authentication and does not consume the invite.',
|
||||
responseSchema: InviteResponseSchema,
|
||||
statusCode: 200,
|
||||
security: [],
|
||||
@@ -53,7 +47,7 @@ export function InviteController(app: HonoApp) {
|
||||
operationId: 'accept_invite',
|
||||
summary: 'Accept invite',
|
||||
description:
|
||||
'Accepts an invite using its code, adding the authenticated user to the corresponding guild, pack, or other entity. The invite usage count is incremented, and if it reaches its maximum usage limit or expiration, the invite is automatically revoked. Returns the accepted invite details.',
|
||||
'Accepts an invite using its code, adding the authenticated user to the corresponding guild or other entity. The invite usage count is incremented, and if it reaches its maximum usage limit or expiration, the invite is automatically revoked. Returns the accepted invite details.',
|
||||
responseSchema: InviteResponseSchema,
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
@@ -77,7 +71,7 @@ export function InviteController(app: HonoApp) {
|
||||
operationId: 'delete_invite',
|
||||
summary: 'Delete invite',
|
||||
description:
|
||||
'Permanently deletes an invite by its code, preventing any further usage. The authenticated user must have permission to manage invites for the guild, channel, or pack associated with the invite. This action can be logged in the audit log if an X-Audit-Log-Reason header is provided.',
|
||||
'Permanently deletes an invite by its code, preventing any further usage. The authenticated user must have permission to manage invites for the guild or channel associated with the invite. This action can be logged in the audit log if an X-Audit-Log-Reason header is provided.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
@@ -171,60 +165,4 @@ export function InviteController(app: HonoApp) {
|
||||
return ctx.json(await inviteRequestService.listGuildInvites({userId, guildId, requestCache}));
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
'/packs/:pack_id/invites',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_INVITES_LIST),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', PackIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'list_pack_invites',
|
||||
summary: 'List pack invites',
|
||||
description:
|
||||
'Retrieves all currently active invites for the specified pack, including invite codes, creators, expiration times, and usage statistics. The authenticated user must have permission to manage invites for the pack and must be a default (non-bot) user. Returns an array of invite metadata objects.',
|
||||
responseSchema: z.array(InviteMetadataResponseSchema),
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: ['Invites'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const packId = createGuildID(ctx.req.valid('param').pack_id);
|
||||
const inviteRequestService = ctx.get('inviteRequestService');
|
||||
const requestCache = ctx.get('requestCache');
|
||||
return ctx.json(await inviteRequestService.listPackInvites({userId, packId, requestCache}));
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
'/packs/:pack_id/invites',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_INVITES_CREATE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', PackIdParam),
|
||||
Validator('json', PackInviteCreateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'create_pack_invite',
|
||||
summary: 'Create pack invite',
|
||||
description:
|
||||
'Creates a new invite for the specified pack with optional parameters such as maximum age and maximum uses. The authenticated user must have permission to create invites for the pack and must be a default (non-bot) user. Returns the created invite with full metadata including usage statistics.',
|
||||
responseSchema: InviteMetadataResponseSchema,
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: ['Invites'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const packId = createGuildID(ctx.req.valid('param').pack_id);
|
||||
const inviteRequestService = ctx.get('inviteRequestService');
|
||||
const requestCache = ctx.get('requestCache');
|
||||
return ctx.json(
|
||||
await inviteRequestService.createPackInvite({
|
||||
inviterId: userId,
|
||||
packId,
|
||||
requestCache,
|
||||
data: ctx.req.valid('json'),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import {InviteTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {UnknownInviteError} from '@fluxer/errors/src/domains/invite/UnknownInviteError';
|
||||
import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError';
|
||||
import type {ChannelPartialResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import type {GuildPartialResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import type {
|
||||
@@ -10,8 +9,6 @@ import type {
|
||||
GroupDmInviteResponse,
|
||||
GuildInviteMetadataResponse,
|
||||
GuildInviteResponse,
|
||||
PackInviteMetadataResponse,
|
||||
PackInviteResponse,
|
||||
} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import type {z} from 'zod';
|
||||
import type {ChannelID, GuildID} from '../BrandedTypes';
|
||||
@@ -20,8 +17,6 @@ import type {UserCacheService} from '../infrastructure/UserCacheService';
|
||||
import type {RequestCache} from '../middleware/RequestCacheMiddleware';
|
||||
import type {Channel} from '../models/Channel';
|
||||
import type {Invite} from '../models/Invite';
|
||||
import {mapPackToSummary} from '../pack/PackModel';
|
||||
import type {PackRepository} from '../pack/PackRepository';
|
||||
import {getCachedUserPartialResponse, getCachedUserPartialResponses} from '../user/UserCacheHelpers';
|
||||
|
||||
interface MapInviteToGuildInviteResponseParams {
|
||||
@@ -216,67 +211,3 @@ export async function mapInviteToGroupDmInviteMetadataResponse({
|
||||
max_uses: invite.maxUses,
|
||||
};
|
||||
}
|
||||
|
||||
interface MapInviteToPackInviteResponseParams {
|
||||
invite: Invite;
|
||||
userCacheService: UserCacheService;
|
||||
requestCache: RequestCache;
|
||||
packRepository: PackRepository;
|
||||
}
|
||||
|
||||
const buildPackInviteBase = async ({
|
||||
invite,
|
||||
userCacheService,
|
||||
requestCache,
|
||||
packRepository,
|
||||
}: MapInviteToPackInviteResponseParams): Promise<z.infer<typeof PackInviteResponse>> => {
|
||||
if (!invite.guildId) {
|
||||
throw new UnknownPackError();
|
||||
}
|
||||
const pack = await packRepository.getPack(invite.guildId);
|
||||
if (!pack) {
|
||||
throw new UnknownPackError();
|
||||
}
|
||||
const creator = await getCachedUserPartialResponse({
|
||||
userId: pack.creatorId,
|
||||
userCacheService,
|
||||
requestCache,
|
||||
});
|
||||
const inviter = invite.inviterId
|
||||
? await getCachedUserPartialResponse({
|
||||
userId: invite.inviterId,
|
||||
userCacheService,
|
||||
requestCache,
|
||||
})
|
||||
: null;
|
||||
const expiresAt = invite.maxAge > 0 ? new Date(invite.createdAt.getTime() + invite.maxAge * 1000) : null;
|
||||
return {
|
||||
code: invite.code,
|
||||
type: invite.type as typeof InviteTypes.EMOJI_PACK | typeof InviteTypes.STICKER_PACK,
|
||||
pack: {
|
||||
...mapPackToSummary(pack),
|
||||
creator,
|
||||
},
|
||||
inviter,
|
||||
expires_at: expiresAt?.toISOString() ?? null,
|
||||
temporary: invite.temporary,
|
||||
};
|
||||
};
|
||||
|
||||
export async function mapInviteToPackInviteResponse(
|
||||
params: MapInviteToPackInviteResponseParams,
|
||||
): Promise<z.infer<typeof PackInviteResponse>> {
|
||||
return buildPackInviteBase(params);
|
||||
}
|
||||
|
||||
export async function mapInviteToPackInviteMetadataResponse(
|
||||
params: MapInviteToPackInviteResponseParams,
|
||||
): Promise<z.infer<typeof PackInviteMetadataResponse>> {
|
||||
const baseResponse = await buildPackInviteBase(params);
|
||||
return {
|
||||
...baseResponse,
|
||||
created_at: params.invite.createdAt.toISOString(),
|
||||
uses: params.invite.uses,
|
||||
max_uses: params.invite.maxUses,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {InviteTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError';
|
||||
import type {ChannelPartialResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import type {GuildPartialResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import type {
|
||||
ChannelInviteCreateRequest,
|
||||
InviteMetadataResponseSchema,
|
||||
InviteResponseSchema,
|
||||
PackInviteCreateRequest,
|
||||
} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import type {ChannelID, GuildID, InviteCode, UserID} from '../BrandedTypes';
|
||||
import {mapChannelToPartialResponse} from '../channel/ChannelMappers';
|
||||
@@ -19,14 +17,11 @@ import type {UserCacheService} from '../infrastructure/UserCacheService';
|
||||
import type {RequestCache} from '../middleware/RequestCacheMiddleware';
|
||||
import type {Channel} from '../models/Channel';
|
||||
import type {Invite} from '../models/Invite';
|
||||
import type {PackRepository} from '../pack/PackRepository';
|
||||
import {
|
||||
mapInviteToGroupDmInviteMetadataResponse,
|
||||
mapInviteToGroupDmInviteResponse,
|
||||
mapInviteToGuildInviteMetadataResponse,
|
||||
mapInviteToGuildInviteResponse,
|
||||
mapInviteToPackInviteMetadataResponse,
|
||||
mapInviteToPackInviteResponse,
|
||||
} from './InviteModel';
|
||||
import type {InviteService} from './InviteService';
|
||||
|
||||
@@ -41,7 +36,6 @@ interface MappingHelpers {
|
||||
memberCount: number;
|
||||
presenceCount: number;
|
||||
}>;
|
||||
packRepository: PackRepository;
|
||||
gatewayService: IGatewayService;
|
||||
}
|
||||
|
||||
@@ -51,7 +45,6 @@ export class InviteRequestService {
|
||||
private readonly channelService: ChannelService,
|
||||
private readonly guildService: GuildService,
|
||||
private readonly gatewayService: IGatewayService,
|
||||
private readonly packRepository: PackRepository,
|
||||
private readonly userCacheService: UserCacheService,
|
||||
) {}
|
||||
|
||||
@@ -127,43 +120,6 @@ export class InviteRequestService {
|
||||
return this.mapInviteList(invites, params.requestCache);
|
||||
}
|
||||
|
||||
async listPackInvites(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Array<InviteMetadataResponseSchema>> {
|
||||
const invites = await this.inviteService.getPackInvitesSorted({
|
||||
userId: params.userId,
|
||||
packId: params.packId,
|
||||
});
|
||||
return this.mapInviteList(invites, params.requestCache);
|
||||
}
|
||||
|
||||
async createPackInvite(params: {
|
||||
inviterId: UserID;
|
||||
packId: GuildID;
|
||||
requestCache: RequestCache;
|
||||
data: PackInviteCreateRequest;
|
||||
}): Promise<InviteMetadataResponseSchema> {
|
||||
const pack = await this.packRepository.getPack(params.packId);
|
||||
if (!pack) {
|
||||
throw new UnknownPackError();
|
||||
}
|
||||
const {invite, isNew} = await this.inviteService.createPackInvite({
|
||||
inviterId: params.inviterId,
|
||||
packId: params.packId,
|
||||
packType: pack.type,
|
||||
maxUses: params.data.max_uses ?? 0,
|
||||
maxAge: params.data.max_age ?? 0,
|
||||
unique: params.data.unique ?? false,
|
||||
});
|
||||
const inviteData = await this.mapInviteMetadataResponse(invite, params.requestCache);
|
||||
if (isNew) {
|
||||
await this.inviteService.dispatchInviteCreate(invite, inviteData);
|
||||
}
|
||||
return inviteData;
|
||||
}
|
||||
|
||||
private createMappingHelpers(requestCache: RequestCache): MappingHelpers {
|
||||
return {
|
||||
userCacheService: this.userCacheService,
|
||||
@@ -176,7 +132,6 @@ export class InviteRequestService {
|
||||
await this.channelService.channelData.operations.getChannelMemberCount(channelId),
|
||||
getGuildResponse: async (guildId: GuildID) => await this.guildService.data.getPublicGuildData(guildId),
|
||||
getGuildCounts: async (guildId: GuildID) => await this.gatewayService.getGuildCounts(guildId),
|
||||
packRepository: this.packRepository,
|
||||
gatewayService: this.gatewayService,
|
||||
};
|
||||
}
|
||||
@@ -186,9 +141,6 @@ export class InviteRequestService {
|
||||
if (invite.type === InviteTypes.GROUP_DM) {
|
||||
return mapInviteToGroupDmInviteResponse({invite, ...helpers});
|
||||
}
|
||||
if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) {
|
||||
return mapInviteToPackInviteResponse({invite, ...helpers});
|
||||
}
|
||||
return mapInviteToGuildInviteResponse({invite, ...helpers});
|
||||
}
|
||||
|
||||
@@ -200,9 +152,6 @@ export class InviteRequestService {
|
||||
if (invite.type === InviteTypes.GROUP_DM) {
|
||||
return mapInviteToGroupDmInviteMetadataResponse({invite, ...helpers});
|
||||
}
|
||||
if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) {
|
||||
return mapInviteToPackInviteMetadataResponse({invite, ...helpers});
|
||||
}
|
||||
return mapInviteToGuildInviteMetadataResponse({invite, ...helpers});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,9 @@ import {MaxGuildInvitesError} from '@fluxer/errors/src/domains/guild/MaxGuildInv
|
||||
import {InvitesDisabledError} from '@fluxer/errors/src/domains/invite/InvitesDisabledError';
|
||||
import {TemporaryInviteRequiresPresenceError} from '@fluxer/errors/src/domains/invite/TemporaryInviteRequiresPresenceError';
|
||||
import {UnknownInviteError} from '@fluxer/errors/src/domains/invite/UnknownInviteError';
|
||||
import {PackAccessDeniedError} from '@fluxer/errors/src/domains/pack/PackAccessDeniedError';
|
||||
import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError';
|
||||
import type {
|
||||
GroupDmInviteMetadataResponse,
|
||||
GuildInviteMetadataResponse,
|
||||
PackInviteMetadataResponse,
|
||||
} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import type {ApiContext} from '../ApiContext';
|
||||
import type {ChannelID, GuildID, InviteCode, UserID} from '../BrandedTypes';
|
||||
@@ -32,8 +29,6 @@ import {createLimitMatchContext} from '../limits/LimitMatchContextBuilder';
|
||||
import type {RequestCache} from '../middleware/RequestCacheMiddleware';
|
||||
import type {Channel} from '../models/Channel';
|
||||
import {Invite} from '../models/Invite';
|
||||
import type {PackRepository, PackType} from '../pack/PackRepository';
|
||||
import type {PackService} from '../pack/PackService';
|
||||
import * as RandomUtils from '../utils/RandomUtils';
|
||||
import type {IInviteRepository} from './IInviteRepository';
|
||||
|
||||
@@ -56,15 +51,6 @@ interface CreateInviteParams {
|
||||
temporary?: boolean;
|
||||
}
|
||||
|
||||
interface CreatePackInviteParams {
|
||||
inviterId: UserID;
|
||||
packId: GuildID;
|
||||
packType: PackType;
|
||||
maxUses: number;
|
||||
maxAge: number;
|
||||
unique: boolean;
|
||||
}
|
||||
|
||||
interface AcceptInviteParams {
|
||||
userId: UserID;
|
||||
inviteCode: InviteCode;
|
||||
@@ -108,11 +94,6 @@ interface ReusableInviteCriteria {
|
||||
type?: number;
|
||||
}
|
||||
|
||||
const PACK_TYPE_TO_INVITE_TYPE: Record<PackType, number> = {
|
||||
emoji: InviteTypes.EMOJI_PACK,
|
||||
sticker: InviteTypes.STICKER_PACK,
|
||||
};
|
||||
|
||||
export class InviteService {
|
||||
constructor(
|
||||
private readonly apiContext: ApiContext,
|
||||
@@ -120,8 +101,6 @@ export class InviteService {
|
||||
private guildService: GuildService,
|
||||
private channelService: ChannelService,
|
||||
private readonly guildAuditLogService: GuildAuditLogService,
|
||||
private readonly packRepository: PackRepository,
|
||||
private readonly packService: PackService,
|
||||
private readonly limitConfigService: LimitConfigService,
|
||||
) {}
|
||||
|
||||
@@ -255,51 +234,6 @@ export class InviteService {
|
||||
return {invite: newInvite, isNew: true};
|
||||
}
|
||||
|
||||
async createPackInvite({inviterId, packId, packType, maxUses, maxAge, unique}: CreatePackInviteParams): Promise<{
|
||||
invite: Invite;
|
||||
isNew: boolean;
|
||||
}> {
|
||||
const pack = await this.packRepository.getPack(packId);
|
||||
if (!pack) {
|
||||
throw new UnknownPackError();
|
||||
}
|
||||
if (pack.creatorId !== inviterId) {
|
||||
throw new PackAccessDeniedError();
|
||||
}
|
||||
if (pack.type !== packType) {
|
||||
throw new PackAccessDeniedError();
|
||||
}
|
||||
const allInvites = await this.inviteRepository.listGuildInvites(packId);
|
||||
const inviteType = PACK_TYPE_TO_INVITE_TYPE[packType];
|
||||
if (!unique) {
|
||||
const existingInvite = this.findReusableInvite(allInvites, {
|
||||
inviterId,
|
||||
maxUses,
|
||||
maxAge,
|
||||
type: inviteType,
|
||||
});
|
||||
if (existingInvite) {
|
||||
return {invite: existingInvite, isNew: false};
|
||||
}
|
||||
}
|
||||
const packInviteLimit = this.resolveInviteLimit(null);
|
||||
if (allInvites.length >= packInviteLimit) {
|
||||
throw new MaxGuildInvitesError(packInviteLimit);
|
||||
}
|
||||
const newInvite = await this.inviteRepository.create({
|
||||
code: this.createRandomInviteCode(),
|
||||
type: inviteType,
|
||||
guild_id: packId,
|
||||
channel_id: null,
|
||||
inviter_id: inviterId,
|
||||
uses: 0,
|
||||
max_uses: maxUses,
|
||||
max_age: maxAge,
|
||||
temporary: false,
|
||||
});
|
||||
return {invite: newInvite, isNew: true};
|
||||
}
|
||||
|
||||
async acceptInvite({userId, inviteCode, requestCache}: AcceptInviteParams): Promise<Invite> {
|
||||
const invite = await this.findInviteWithLowercaseFallback(inviteCode);
|
||||
if (!invite) throw new UnknownInviteError();
|
||||
@@ -334,11 +268,6 @@ export class InviteService {
|
||||
});
|
||||
return this.incrementInviteUses(invite, {deleteWhenExhausted: true});
|
||||
}
|
||||
if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) {
|
||||
if (!invite.guildId) throw new UnknownInviteError();
|
||||
await this.packService.installPack(userId, invite.guildId);
|
||||
return this.incrementInviteUses(invite, {deleteWhenExhausted: true});
|
||||
}
|
||||
if (!invite.guildId) throw new UnknownInviteError();
|
||||
const guild = await this.guildService.data.getGuildSystem(invite.guildId);
|
||||
if ((guild.disabledOperations & GuildOperations.INSTANT_INVITES) !== 0) {
|
||||
@@ -433,18 +362,6 @@ export class InviteService {
|
||||
async deleteInvite({userId, inviteCode}: DeleteInviteParams, auditLogReason?: string | null): Promise<void> {
|
||||
const invite = await this.findInviteWithLowercaseFallback(inviteCode);
|
||||
if (!invite) throw new UnknownInviteError();
|
||||
if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) {
|
||||
if (!invite.guildId) throw new UnknownInviteError();
|
||||
const pack = await this.packRepository.getPack(invite.guildId);
|
||||
if (!pack) {
|
||||
throw new UnknownPackError();
|
||||
}
|
||||
if (pack.creatorId !== userId) {
|
||||
throw new PackAccessDeniedError();
|
||||
}
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
return;
|
||||
}
|
||||
if (invite.type === InviteTypes.GROUP_DM) {
|
||||
if (!invite.channelId) throw new UnknownInviteError();
|
||||
const channel = await this.channelService.channelData.operations.getChannel({
|
||||
@@ -497,25 +414,9 @@ export class InviteService {
|
||||
return invites.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}
|
||||
|
||||
async getPackInvitesSorted(params: {userId: UserID; packId: GuildID}): Promise<Array<Invite>> {
|
||||
const {userId, packId} = params;
|
||||
const pack = await this.packRepository.getPack(packId);
|
||||
if (!pack) {
|
||||
throw new UnknownPackError();
|
||||
}
|
||||
if (pack.creatorId !== userId) {
|
||||
throw new PackAccessDeniedError();
|
||||
}
|
||||
const invites = await this.inviteRepository.listGuildInvites(packId);
|
||||
const inviteType = PACK_TYPE_TO_INVITE_TYPE[pack.type];
|
||||
return invites
|
||||
.filter((invite) => invite.type === inviteType)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}
|
||||
|
||||
async dispatchInviteCreate(
|
||||
invite: Invite,
|
||||
inviteData: GuildInviteMetadataResponse | GroupDmInviteMetadataResponse | PackInviteMetadataResponse,
|
||||
inviteData: GuildInviteMetadataResponse | GroupDmInviteMetadataResponse,
|
||||
): Promise<void> {
|
||||
if (invite.guildId && invite.type === InviteTypes.GUILD) {
|
||||
await this.apiContext.services.gateway.dispatchGuild({
|
||||
|
||||
@@ -9,7 +9,6 @@ import {ChannelService} from '../channel/services/ChannelService';
|
||||
import type {IFavoriteMemeRepository} from '../favorite_meme/IFavoriteMemeRepository';
|
||||
import type {GuildAuditLogService} from '../guild/GuildAuditLogService';
|
||||
import type {IGuildRepositoryAggregate} from '../guild/repositories/IGuildRepositoryAggregate';
|
||||
import type {ExpressionAssetPurger} from '../guild/services/content/ExpressionAssetPurger';
|
||||
import {GuildService} from '../guild/services/GuildService';
|
||||
import type {AvatarService} from '../infrastructure/AvatarService';
|
||||
import type {IPurgeQueue} from '../infrastructure/BunnyPurgeQueue';
|
||||
@@ -23,8 +22,6 @@ import type {UserCacheService} from '../infrastructure/UserCacheService';
|
||||
import type {InviteRepository} from '../invite/InviteRepository';
|
||||
import {InviteService} from '../invite/InviteService';
|
||||
import type {LimitConfigService} from '../limits/LimitConfigService';
|
||||
import type {PackRepository} from '../pack/PackRepository';
|
||||
import {PackService} from '../pack/PackService';
|
||||
import type {ReadStateService} from '../read_state/ReadStateService';
|
||||
import type {IUserRepository} from '../user/IUserRepository';
|
||||
import type {VoiceAvailabilityService} from '../voice/VoiceAvailabilityService';
|
||||
@@ -32,7 +29,6 @@ import type {IWebhookRepository} from '../webhook/IWebhookRepository';
|
||||
|
||||
interface GuildStackServiceFactoryDependencies {
|
||||
apiContext: ApiContext;
|
||||
packRepository: PackRepository;
|
||||
channelRepository: IChannelRepository;
|
||||
userRepository: IUserRepository;
|
||||
guildRepository: IGuildRepositoryAggregate;
|
||||
@@ -42,7 +38,6 @@ interface GuildStackServiceFactoryDependencies {
|
||||
avatarService: AvatarService;
|
||||
entityAssetService: EntityAssetService;
|
||||
assetDeletionQueue: IAssetDeletionQueue;
|
||||
expressionAssetPurger: ExpressionAssetPurger;
|
||||
userCacheService: UserCacheService;
|
||||
limitConfigService: LimitConfigService;
|
||||
embedService: EmbedService;
|
||||
@@ -59,40 +54,24 @@ interface GuildStackServiceFactoryDependencies {
|
||||
}
|
||||
|
||||
export interface GuildStackServices {
|
||||
packService: PackService;
|
||||
channelService: ChannelService;
|
||||
guildService: GuildService;
|
||||
inviteService: InviteService;
|
||||
}
|
||||
|
||||
class LazyGuildStackServices implements GuildStackServices {
|
||||
private cachedPackService: PackService | undefined;
|
||||
private cachedChannelService: ChannelService | undefined;
|
||||
private cachedGuildService: GuildService | undefined;
|
||||
private cachedInviteService: InviteService | undefined;
|
||||
|
||||
constructor(private readonly dependencies: GuildStackServiceFactoryDependencies) {}
|
||||
|
||||
get packService(): PackService {
|
||||
this.cachedPackService ??= new PackService(
|
||||
this.dependencies.apiContext,
|
||||
this.dependencies.packRepository,
|
||||
this.dependencies.guildRepository,
|
||||
this.dependencies.avatarService,
|
||||
this.dependencies.expressionAssetPurger,
|
||||
this.dependencies.userCacheService,
|
||||
this.dependencies.limitConfigService,
|
||||
);
|
||||
return this.cachedPackService;
|
||||
}
|
||||
|
||||
get channelService(): ChannelService {
|
||||
this.cachedChannelService ??= new ChannelService(
|
||||
this.dependencies.apiContext,
|
||||
this.dependencies.channelRepository,
|
||||
this.dependencies.userRepository,
|
||||
this.dependencies.guildRepository,
|
||||
this.packService,
|
||||
this.dependencies.userCacheService,
|
||||
this.dependencies.embedService,
|
||||
this.dependencies.readStateService,
|
||||
@@ -139,8 +118,6 @@ class LazyGuildStackServices implements GuildStackServices {
|
||||
this.guildService,
|
||||
this.channelService,
|
||||
this.dependencies.guildAuditLogService,
|
||||
this.dependencies.packRepository,
|
||||
this.packService,
|
||||
this.dependencies.limitConfigService,
|
||||
);
|
||||
return this.cachedInviteService;
|
||||
|
||||
@@ -124,7 +124,6 @@ import {
|
||||
getEntranceSoundPlayService,
|
||||
getEntranceSoundService,
|
||||
getErrorI18nService,
|
||||
getExpressionAssetPurger,
|
||||
getFavoriteMemeRepository,
|
||||
getGatewayRequestService,
|
||||
getGifService,
|
||||
@@ -139,7 +138,6 @@ import {
|
||||
getLimitConfigService,
|
||||
getNcmecSubmissionService,
|
||||
getOAuth2TokenRepository,
|
||||
getPackRepository,
|
||||
getPasswordChangeRepository,
|
||||
getPremiumStateReconciliationQueueService,
|
||||
getPurgeQueue,
|
||||
@@ -444,7 +442,6 @@ class RequestServices implements RequestScopedServices {
|
||||
private get guildStack(): GuildStackServices {
|
||||
this.cachedGuildStack ??= createGuildStackServices({
|
||||
apiContext: this.context,
|
||||
packRepository: getPackRepository(),
|
||||
channelRepository: this.channelRepository,
|
||||
userRepository: getUserRepository(),
|
||||
guildRepository: this.requestGuildRepository,
|
||||
@@ -454,7 +451,6 @@ class RequestServices implements RequestScopedServices {
|
||||
avatarService: getAvatarService(),
|
||||
entityAssetService: getEntityAssetService(),
|
||||
assetDeletionQueue: getAssetDeletionQueue(),
|
||||
expressionAssetPurger: getExpressionAssetPurger(),
|
||||
userCacheService: getUserCacheService(),
|
||||
limitConfigService: getLimitConfigService(),
|
||||
embedService: getEmbedService(),
|
||||
@@ -472,10 +468,6 @@ class RequestServices implements RequestScopedServices {
|
||||
return this.cachedGuildStack;
|
||||
}
|
||||
|
||||
get packService() {
|
||||
return this.guildStack.packService;
|
||||
}
|
||||
|
||||
get channelService() {
|
||||
return this.guildStack.channelService;
|
||||
}
|
||||
@@ -583,10 +575,6 @@ class RequestServices implements RequestScopedServices {
|
||||
return getOAuth2TokenRepository();
|
||||
}
|
||||
|
||||
get packRepository() {
|
||||
return getPackRepository();
|
||||
}
|
||||
|
||||
get rateLimitService() {
|
||||
return getRateLimitService();
|
||||
}
|
||||
@@ -822,7 +810,6 @@ class RequestServices implements RequestScopedServices {
|
||||
this.channelService,
|
||||
this.guildService,
|
||||
this.gatewayService,
|
||||
getPackRepository(),
|
||||
getUserCacheService(),
|
||||
);
|
||||
return this.cachedInviteRequestService;
|
||||
|
||||
@@ -43,7 +43,6 @@ import {createNatsGifProvider} from '../gif/NatsGifProvider';
|
||||
import {GuildAuditLogService} from '../guild/GuildAuditLogService';
|
||||
import {GuildDiscoveryRepository} from '../guild/repositories/GuildDiscoveryRepository';
|
||||
import {GuildRepository} from '../guild/repositories/GuildRepository';
|
||||
import {ExpressionAssetPurger} from '../guild/services/content/ExpressionAssetPurger';
|
||||
import {GuildDiscoveryService} from '../guild/services/GuildDiscoveryService';
|
||||
import {AssetDeletionQueue} from '../infrastructure/AssetDeletionQueue';
|
||||
import {AvatarService} from '../infrastructure/AvatarService';
|
||||
@@ -75,7 +74,6 @@ import {BotAuthService} from '../oauth/BotAuthService';
|
||||
import {BotMfaMirrorService} from '../oauth/BotMfaMirrorService';
|
||||
import {ApplicationRepository} from '../oauth/repositories/ApplicationRepository';
|
||||
import {OAuth2TokenRepository} from '../oauth/repositories/OAuth2TokenRepository';
|
||||
import {PackRepository} from '../pack/PackRepository';
|
||||
import {ReadStateRepository} from '../read_state/ReadStateRepository';
|
||||
import {ReadStateRequestService} from '../read_state/ReadStateRequestService';
|
||||
import {ReadStateService} from '../read_state/ReadStateService';
|
||||
@@ -119,7 +117,6 @@ export const getAdminArchiveRepository = singleton(() => new AdminArchiveReposit
|
||||
export const getVoiceRepository = singleton(() => new VoiceRepository());
|
||||
export const getApplicationRepository = singleton(() => new ApplicationRepository());
|
||||
export const getOAuth2TokenRepository = singleton(() => new OAuth2TokenRepository());
|
||||
export const getPackRepository = singleton(() => new PackRepository());
|
||||
export const getGuildDiscoveryRepository = singleton(() => new GuildDiscoveryRepository());
|
||||
export const getEmailChangeRepository = singleton(() => new EmailChangeRepository());
|
||||
export const getPasswordChangeRepository = singleton(() => new PasswordChangeRepository());
|
||||
@@ -398,7 +395,6 @@ export const getGifService = singleton(() => {
|
||||
createNatsGifProvider(async () => (await instanceConfigRepository.getEffectiveGifConfig()).klipy_api_key),
|
||||
);
|
||||
});
|
||||
export const getExpressionAssetPurger = singleton(() => new ExpressionAssetPurger(getAssetDeletionQueue()));
|
||||
export const getGuildAuditLogService = singleton(
|
||||
() => new GuildAuditLogService(getGuildRepository(), getSnowflakeService(), getWorkerService(), getGatewayService()),
|
||||
);
|
||||
|
||||
@@ -54,8 +54,6 @@ const REQUEST_SERVICE_VARIABLES: ReadonlyArray<keyof HonoEnv['Variables']> = [
|
||||
'oauth2RequestService',
|
||||
'oauth2Service',
|
||||
'oauth2TokenRepository',
|
||||
'packRepository',
|
||||
'packService',
|
||||
'passwordChangeService',
|
||||
'rateLimitService',
|
||||
'readStateRequestService',
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {GuildID, UserID} from '../BrandedTypes';
|
||||
import type {ExpressionPackRow} from '../database/types/UserTypes';
|
||||
|
||||
type ExpressionPackType = 'emoji' | 'sticker';
|
||||
|
||||
export class ExpressionPack {
|
||||
readonly id: GuildID;
|
||||
readonly type: ExpressionPackType;
|
||||
readonly creatorId: UserID;
|
||||
readonly name: string;
|
||||
readonly description: string | null;
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt: Date;
|
||||
readonly version: number;
|
||||
|
||||
constructor(row: ExpressionPackRow) {
|
||||
this.id = row.pack_id;
|
||||
this.type = row.pack_type as ExpressionPackType;
|
||||
this.creatorId = row.creator_id;
|
||||
this.name = row.name;
|
||||
this.description = row.description ?? null;
|
||||
this.createdAt = row.created_at;
|
||||
this.updatedAt = row.updated_at;
|
||||
this.version = row.version;
|
||||
}
|
||||
|
||||
toRow(): ExpressionPackRow {
|
||||
return {
|
||||
pack_id: this.id,
|
||||
pack_type: this.type,
|
||||
creator_id: this.creatorId,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
created_at: this.createdAt,
|
||||
updated_at: this.updatedAt,
|
||||
version: this.version,
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {GuildID, UserID} from '../BrandedTypes';
|
||||
import type {PackType} from './PackRepository';
|
||||
|
||||
export type PackExpressionAccessResolution = 'accessible' | 'not-accessible' | 'not-pack';
|
||||
|
||||
export interface PackExpressionAccessResolver {
|
||||
resolve(packId: GuildID): Promise<PackExpressionAccessResolution>;
|
||||
}
|
||||
|
||||
export interface PackExpressionAccessResolverParams {
|
||||
userId: UserID | null;
|
||||
type: PackType;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
import type {ExpressionPack} from '../models/ExpressionPack';
|
||||
|
||||
export function mapPackToSummary(pack: ExpressionPack, installedAt?: Date | null): PackSummaryResponse {
|
||||
const summary: PackSummaryResponse = {
|
||||
id: pack.id.toString(),
|
||||
name: pack.name,
|
||||
description: pack.description,
|
||||
type: pack.type,
|
||||
creator_id: pack.creatorId.toString(),
|
||||
created_at: pack.createdAt.toISOString(),
|
||||
updated_at: pack.updatedAt.toISOString(),
|
||||
};
|
||||
if (installedAt) {
|
||||
summary.installed_at = installedAt.toISOString();
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {GuildID, UserID} from '../BrandedTypes';
|
||||
import {BatchBuilder, fetchMany, fetchOne, upsertOne} from '../database/CassandraQueryExecution';
|
||||
import {nextVersion} from '../database/CassandraTypes';
|
||||
import {buildPatchFromData, executeVersionedUpdate} from '../database/CassandraVersionedUpdate';
|
||||
import type {ExpressionPackRow, PackInstallationRow} from '../database/types/UserTypes';
|
||||
import {EXPRESSION_PACK_COLUMNS} from '../database/types/UserTypes';
|
||||
import {ExpressionPack} from '../models/ExpressionPack';
|
||||
import {ExpressionPacks, ExpressionPacksByCreator, PackInstallations} from '../Tables';
|
||||
|
||||
export type PackType = ExpressionPack['type'];
|
||||
|
||||
const FETCH_EXPRESSION_PACK_BY_ID_QUERY = ExpressionPacks.select({
|
||||
where: ExpressionPacks.where.eq('pack_id'),
|
||||
limit: 1,
|
||||
});
|
||||
const FETCH_EXPRESSION_PACKS_BY_CREATOR_QUERY = ExpressionPacksByCreator.select({
|
||||
where: ExpressionPacksByCreator.where.eq('creator_id'),
|
||||
});
|
||||
const FETCH_PACK_INSTALLATIONS_BY_USER_QUERY = PackInstallations.select({
|
||||
where: PackInstallations.where.eq('user_id'),
|
||||
});
|
||||
const FETCH_PACK_INSTALLATION_QUERY = PackInstallations.select({
|
||||
where: [PackInstallations.where.eq('user_id'), PackInstallations.where.eq('pack_id')],
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
export class PackRepository {
|
||||
async getPack(packId: GuildID): Promise<ExpressionPack | null> {
|
||||
const row = await fetchOne<ExpressionPackRow>(FETCH_EXPRESSION_PACK_BY_ID_QUERY.bind({pack_id: packId}));
|
||||
return row ? new ExpressionPack(row) : null;
|
||||
}
|
||||
|
||||
async listPacksByCreator(creatorId: UserID, packType?: PackType): Promise<Array<ExpressionPack>> {
|
||||
const rows = await fetchMany<ExpressionPackRow>(
|
||||
FETCH_EXPRESSION_PACKS_BY_CREATOR_QUERY.bind({creator_id: creatorId}),
|
||||
);
|
||||
return rows.filter((row) => (packType ? row.pack_type === packType : true)).map((row) => new ExpressionPack(row));
|
||||
}
|
||||
|
||||
async countPacksByCreator(creatorId: UserID, packType: PackType): Promise<number> {
|
||||
const rows = await fetchMany<ExpressionPackRow>(
|
||||
FETCH_EXPRESSION_PACKS_BY_CREATOR_QUERY.bind({creator_id: creatorId}),
|
||||
);
|
||||
return rows.filter((row) => row.pack_type === packType).length;
|
||||
}
|
||||
|
||||
async upsertPack(data: ExpressionPackRow): Promise<ExpressionPack> {
|
||||
const packId = data.pack_id;
|
||||
const previousPack = await this.getPack(packId);
|
||||
const result = await executeVersionedUpdate<ExpressionPackRow, 'pack_id'>(
|
||||
async () => {
|
||||
const existing = await fetchOne<ExpressionPackRow>(FETCH_EXPRESSION_PACK_BY_ID_QUERY.bind({pack_id: packId}));
|
||||
return existing ?? null;
|
||||
},
|
||||
(current) => ({
|
||||
pk: {pack_id: packId},
|
||||
patch: buildPatchFromData(data, current, EXPRESSION_PACK_COLUMNS, ['pack_id']),
|
||||
}),
|
||||
ExpressionPacks,
|
||||
);
|
||||
const batch = new BatchBuilder();
|
||||
if (previousPack && previousPack.creatorId !== data.creator_id) {
|
||||
batch.addPrepared(
|
||||
ExpressionPacksByCreator.deleteByPk({
|
||||
creator_id: previousPack.creatorId,
|
||||
pack_id: packId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const finalPack = new ExpressionPack({...data, version: result.finalVersion ?? nextVersion(previousPack?.version)});
|
||||
batch.addPrepared(ExpressionPacksByCreator.insert(finalPack.toRow()));
|
||||
await batch.execute();
|
||||
return finalPack;
|
||||
}
|
||||
|
||||
async deletePack(packId: GuildID): Promise<void> {
|
||||
const pack = await this.getPack(packId);
|
||||
const batch = new BatchBuilder().addPrepared(ExpressionPacks.deleteByPk({pack_id: packId}));
|
||||
if (pack) {
|
||||
batch.addPrepared(
|
||||
ExpressionPacksByCreator.deleteByPk({
|
||||
creator_id: pack.creatorId,
|
||||
pack_id: packId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await batch.execute();
|
||||
}
|
||||
|
||||
async listInstallations(userId: UserID): Promise<Array<PackInstallationRow>> {
|
||||
return await fetchMany<PackInstallationRow>(FETCH_PACK_INSTALLATIONS_BY_USER_QUERY.bind({user_id: userId}));
|
||||
}
|
||||
|
||||
async addInstallation(data: PackInstallationRow): Promise<void> {
|
||||
await upsertOne(PackInstallations.insert(data));
|
||||
}
|
||||
|
||||
async removeInstallation(userId: UserID, packId: GuildID): Promise<void> {
|
||||
await PackInstallations.deleteByPk({
|
||||
user_id: userId,
|
||||
pack_id: packId,
|
||||
});
|
||||
}
|
||||
|
||||
async hasInstallation(userId: UserID, packId: GuildID): Promise<boolean> {
|
||||
const row = await fetchOne<PackInstallationRow>(
|
||||
FETCH_PACK_INSTALLATION_QUERY.bind({
|
||||
user_id: userId,
|
||||
pack_id: packId,
|
||||
}),
|
||||
);
|
||||
return row !== null;
|
||||
}
|
||||
}
|
||||
@@ -1,642 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {
|
||||
MAX_CREATED_PACKS_NON_PREMIUM,
|
||||
MAX_INSTALLED_PACKS_NON_PREMIUM,
|
||||
MAX_PACK_EXPRESSIONS,
|
||||
} from '@fluxer/constants/src/LimitConstants';
|
||||
import {UserFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {FeatureAccessError} from '@fluxer/errors/src/domains/core/FeatureAccessError';
|
||||
import {FeatureTemporarilyDisabledError} from '@fluxer/errors/src/domains/core/FeatureTemporarilyDisabledError';
|
||||
import {UnknownGuildEmojiError} from '@fluxer/errors/src/domains/guild/UnknownGuildEmojiError';
|
||||
import {UnknownGuildStickerError} from '@fluxer/errors/src/domains/guild/UnknownGuildStickerError';
|
||||
import {InvalidPackTypeError} from '@fluxer/errors/src/domains/pack/InvalidPackTypeError';
|
||||
import {MaxPackExpressionsError} from '@fluxer/errors/src/domains/pack/MaxPackExpressionsError';
|
||||
import {MaxPackLimitError} from '@fluxer/errors/src/domains/pack/MaxPackLimitError';
|
||||
import {PackAccessDeniedError} from '@fluxer/errors/src/domains/pack/PackAccessDeniedError';
|
||||
import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError';
|
||||
import {FluxerError} from '@fluxer/errors/src/FluxerError';
|
||||
import {getErrorMessageUnsafe} from '@fluxer/errors/src/i18n/ErrorI18n';
|
||||
import type {
|
||||
GuildEmojiResponse,
|
||||
GuildEmojiWithUserResponse,
|
||||
GuildStickerResponse,
|
||||
GuildStickerWithUserResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
import type {PackDashboardResponse, PackDashboardSectionResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
import type {ApiContext} from '../ApiContext';
|
||||
import type {EmojiID, GuildID, StickerID, UserID} from '../BrandedTypes';
|
||||
import {createEmojiID, createGuildID, createStickerID} from '../BrandedTypes';
|
||||
import {
|
||||
mapGuildEmojisWithUsersToResponse,
|
||||
mapGuildEmojiToResponse,
|
||||
mapGuildStickersWithUsersToResponse,
|
||||
mapGuildStickerToResponse,
|
||||
} from '../guild/GuildModel';
|
||||
import type {IGuildRepositoryAggregate} from '../guild/repositories/IGuildRepositoryAggregate';
|
||||
import type {ExpressionAssetPurger} from '../guild/services/content/ExpressionAssetPurger';
|
||||
import type {AvatarService} from '../infrastructure/AvatarService';
|
||||
import type {UserCacheService} from '../infrastructure/UserCacheService';
|
||||
import type {LimitConfigService} from '../limits/LimitConfigService';
|
||||
import {resolveLimitSafe} from '../limits/LimitConfigUtils';
|
||||
import {createLimitMatchContext} from '../limits/LimitMatchContextBuilder';
|
||||
import type {RequestCache} from '../middleware/RequestCacheMiddleware';
|
||||
import {ExpressionPack} from '../models/ExpressionPack';
|
||||
import type {User} from '../models/User';
|
||||
import type {
|
||||
PackExpressionAccessResolution,
|
||||
PackExpressionAccessResolver,
|
||||
PackExpressionAccessResolverParams,
|
||||
} from './PackExpressionAccessResolver';
|
||||
import {mapPackToSummary} from './PackModel';
|
||||
import type {PackRepository, PackType} from './PackRepository';
|
||||
|
||||
export class PackService {
|
||||
constructor(
|
||||
private readonly apiContext: ApiContext,
|
||||
private readonly packRepository: PackRepository,
|
||||
private readonly guildRepository: IGuildRepositoryAggregate,
|
||||
private readonly avatarService: AvatarService,
|
||||
private readonly assetPurger: ExpressionAssetPurger,
|
||||
private readonly userCacheService: UserCacheService,
|
||||
private readonly limitConfigService: LimitConfigService,
|
||||
) {}
|
||||
|
||||
private getLocalizedPackExpressionLimitMessage(locale: string | null | undefined, count: number): string {
|
||||
return getErrorMessageUnsafe(APIErrorCodes.MAX_PACK_EXPRESSIONS, locale, {count});
|
||||
}
|
||||
|
||||
private getLocalizedBulkErrorMessage(error: unknown, locale: string | null | undefined): string {
|
||||
if (error instanceof FluxerError) {
|
||||
return getErrorMessageUnsafe(error.code, locale, error.messageVariables, error.message);
|
||||
}
|
||||
return getErrorMessageUnsafe(APIErrorCodes.GENERAL_ERROR, locale);
|
||||
}
|
||||
|
||||
private async requireExpressionPackAccess(userId: UserID): Promise<void> {
|
||||
const user = await this.apiContext.services.users.findUnique(userId);
|
||||
if (!user || (user.flags & UserFlags.STAFF) === 0n) {
|
||||
throw new FeatureTemporarilyDisabledError();
|
||||
}
|
||||
}
|
||||
|
||||
private async hasFeatureAccess(userId: UserID, limitKey: LimitKey): Promise<boolean> {
|
||||
const user = await this.apiContext.services.users.findUnique(userId);
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
const ctx = createLimitMatchContext({user});
|
||||
const value = resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, limitKey, 0);
|
||||
return value > 0;
|
||||
}
|
||||
|
||||
private async ensurePackOwner(userId: UserID, packId: GuildID): Promise<ExpressionPack> {
|
||||
const pack = await this.packRepository.getPack(packId);
|
||||
if (!pack) {
|
||||
throw new UnknownPackError();
|
||||
}
|
||||
if (pack.creatorId !== userId) {
|
||||
throw new PackAccessDeniedError();
|
||||
}
|
||||
return pack;
|
||||
}
|
||||
|
||||
private async collectInstalledPacks(userId: UserID): Promise<
|
||||
Array<{
|
||||
pack: ExpressionPack;
|
||||
installedAt: Date;
|
||||
}>
|
||||
> {
|
||||
const installations = await this.packRepository.listInstallations(userId);
|
||||
const results: Array<{
|
||||
pack: ExpressionPack;
|
||||
installedAt: Date;
|
||||
}> = [];
|
||||
for (const row of installations) {
|
||||
const pack = await this.packRepository.getPack(row.pack_id);
|
||||
if (!pack) {
|
||||
await this.packRepository.removeInstallation(userId, row.pack_id);
|
||||
continue;
|
||||
}
|
||||
results.push({pack, installedAt: row.installed_at});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private async requireFeature(userId: UserID, limitKey: LimitKey): Promise<void> {
|
||||
if (!(await this.hasFeatureAccess(userId, limitKey))) {
|
||||
throw new FeatureAccessError();
|
||||
}
|
||||
}
|
||||
|
||||
private async getInstalledPackIdsByType(userId: UserID, packType: PackType): Promise<Set<GuildID>> {
|
||||
const installations = await this.packRepository.listInstallations(userId);
|
||||
return new Set(installations.filter((row) => row.pack_type === packType).map((row) => row.pack_id));
|
||||
}
|
||||
|
||||
private buildPackExpressionAccessResolution(
|
||||
userId: UserID,
|
||||
packType: PackType,
|
||||
pack: ExpressionPack | null,
|
||||
): PackExpressionAccessResolution {
|
||||
if (!pack) {
|
||||
return 'not-pack';
|
||||
}
|
||||
if (pack.type !== packType) {
|
||||
return 'not-pack';
|
||||
}
|
||||
return pack.creatorId === userId ? 'accessible' : 'not-accessible';
|
||||
}
|
||||
|
||||
async createPackExpressionAccessResolver(
|
||||
params: PackExpressionAccessResolverParams,
|
||||
): Promise<PackExpressionAccessResolver> {
|
||||
const {userId, type} = params;
|
||||
if (!userId) {
|
||||
return {
|
||||
resolve: async () => 'not-pack',
|
||||
};
|
||||
}
|
||||
const installedPackIds = await this.getInstalledPackIdsByType(userId, type);
|
||||
const resolutionCache = new Map<GuildID, PackExpressionAccessResolution>();
|
||||
return {
|
||||
resolve: async (packId: GuildID) => {
|
||||
if (installedPackIds.has(packId)) {
|
||||
resolutionCache.set(packId, 'accessible');
|
||||
return 'accessible';
|
||||
}
|
||||
const cached = resolutionCache.get(packId);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const pack = await this.packRepository.getPack(packId);
|
||||
const resolution = this.buildPackExpressionAccessResolution(userId, type, pack);
|
||||
resolutionCache.set(packId, resolution);
|
||||
return resolution;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async listUserPacks(userId: UserID): Promise<PackDashboardResponse> {
|
||||
await this.requireExpressionPackAccess(userId);
|
||||
const user = await this.apiContext.services.users.findUnique(userId);
|
||||
const ctx = createLimitMatchContext({user});
|
||||
resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, 'feature_global_expressions', 0);
|
||||
const createdEmoji = await this.packRepository.listPacksByCreator(userId, 'emoji');
|
||||
const createdSticker = await this.packRepository.listPacksByCreator(userId, 'sticker');
|
||||
const installations = await this.collectInstalledPacks(userId);
|
||||
const installedEmoji = installations
|
||||
.filter((entry) => entry.pack.type === 'emoji')
|
||||
.map((entry) => mapPackToSummary(entry.pack, entry.installedAt));
|
||||
const installedSticker = installations
|
||||
.filter((entry) => entry.pack.type === 'sticker')
|
||||
.map((entry) => mapPackToSummary(entry.pack, entry.installedAt));
|
||||
const fallbackCreatedLimit = MAX_CREATED_PACKS_NON_PREMIUM;
|
||||
const fallbackInstalledLimit = MAX_INSTALLED_PACKS_NON_PREMIUM;
|
||||
const createdLimit = this.resolveLimitForUser(user ?? null, 'max_created_packs', fallbackCreatedLimit);
|
||||
const installedLimit = this.resolveLimitForUser(user ?? null, 'max_installed_packs', fallbackInstalledLimit);
|
||||
const emojiSection: PackDashboardSectionResponse = {
|
||||
installed_limit: installedLimit,
|
||||
created_limit: createdLimit,
|
||||
installed: installedEmoji,
|
||||
created: createdEmoji.map((pack) => mapPackToSummary(pack)),
|
||||
};
|
||||
const stickerSection: PackDashboardSectionResponse = {
|
||||
installed_limit: installedLimit,
|
||||
created_limit: createdLimit,
|
||||
installed: installedSticker,
|
||||
created: createdSticker.map((pack) => mapPackToSummary(pack)),
|
||||
};
|
||||
return {
|
||||
emoji: emojiSection,
|
||||
sticker: stickerSection,
|
||||
};
|
||||
}
|
||||
|
||||
async createPack(params: {
|
||||
user: User;
|
||||
type: PackType;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}): Promise<ExpressionPack> {
|
||||
await this.requireExpressionPackAccess(params.user.id);
|
||||
await this.requireFeature(params.user.id, 'feature_global_expressions');
|
||||
const createdCount = await this.packRepository.countPacksByCreator(params.user.id, params.type);
|
||||
const fallbackLimit = MAX_CREATED_PACKS_NON_PREMIUM;
|
||||
const limit = this.resolveLimitForUser(params.user, 'max_created_packs', fallbackLimit);
|
||||
if (createdCount >= limit) {
|
||||
throw new MaxPackLimitError(params.type, limit, 'create');
|
||||
}
|
||||
const now = new Date();
|
||||
const packId = createGuildID(await this.apiContext.services.snowflake.generate());
|
||||
return await this.packRepository.upsertPack({
|
||||
pack_id: packId,
|
||||
pack_type: params.type,
|
||||
creator_id: params.user.id,
|
||||
name: params.name,
|
||||
description: params.description ?? null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
version: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async updatePack(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
}): Promise<ExpressionPack> {
|
||||
await this.requireExpressionPackAccess(params.userId);
|
||||
const pack = await this.ensurePackOwner(params.userId, params.packId);
|
||||
const now = new Date();
|
||||
const updatedPack = new ExpressionPack({
|
||||
...pack.toRow(),
|
||||
name: params.name ?? pack.name,
|
||||
description: params.description === undefined ? pack.description : params.description,
|
||||
updated_at: now,
|
||||
});
|
||||
return await this.packRepository.upsertPack(updatedPack.toRow());
|
||||
}
|
||||
|
||||
async deletePack(userId: UserID, packId: GuildID): Promise<void> {
|
||||
await this.requireExpressionPackAccess(userId);
|
||||
await this.ensurePackOwner(userId, packId);
|
||||
await this.packRepository.deletePack(packId);
|
||||
}
|
||||
|
||||
async installPack(userId: UserID, packId: GuildID): Promise<void> {
|
||||
await this.requireExpressionPackAccess(userId);
|
||||
const pack = await this.packRepository.getPack(packId);
|
||||
if (!pack) {
|
||||
throw new UnknownPackError();
|
||||
}
|
||||
const alreadyInstalled = await this.packRepository.hasInstallation(userId, packId);
|
||||
if (alreadyInstalled) {
|
||||
return;
|
||||
}
|
||||
await this.requireFeature(userId, 'feature_global_expressions');
|
||||
const user = await this.apiContext.services.users.findUnique(userId);
|
||||
const fallbackLimit = MAX_INSTALLED_PACKS_NON_PREMIUM;
|
||||
const limit = this.resolveLimitForUser(user ?? null, 'max_installed_packs', fallbackLimit);
|
||||
const installations = await this.collectInstalledPacks(userId);
|
||||
const typeCount = installations.filter((entry) => entry.pack.type === pack.type).length;
|
||||
if (typeCount >= limit) {
|
||||
throw new MaxPackLimitError(pack.type, limit, 'install');
|
||||
}
|
||||
await this.packRepository.addInstallation({
|
||||
user_id: userId,
|
||||
pack_id: packId,
|
||||
pack_type: pack.type,
|
||||
installed_at: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
async uninstallPack(userId: UserID, packId: GuildID): Promise<void> {
|
||||
await this.requireExpressionPackAccess(userId);
|
||||
await this.packRepository.removeInstallation(userId, packId);
|
||||
}
|
||||
|
||||
async getInstalledPackIds(userId: UserID): Promise<Set<GuildID>> {
|
||||
await this.requireExpressionPackAccess(userId);
|
||||
const installations = await this.collectInstalledPacks(userId);
|
||||
return new Set(installations.map((entry) => entry.pack.id));
|
||||
}
|
||||
|
||||
async getPackEmojis(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Array<GuildEmojiWithUserResponse>> {
|
||||
await this.requireExpressionPackAccess(params.userId);
|
||||
const pack = await this.ensurePackOwner(params.userId, params.packId);
|
||||
if (pack.type !== 'emoji') {
|
||||
throw new InvalidPackTypeError('emoji');
|
||||
}
|
||||
const emojis = await this.guildRepository.listEmojis(pack.id);
|
||||
return await mapGuildEmojisWithUsersToResponse(emojis, this.userCacheService, params.requestCache);
|
||||
}
|
||||
|
||||
async getPackStickers(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Array<GuildStickerWithUserResponse>> {
|
||||
await this.requireExpressionPackAccess(params.userId);
|
||||
const pack = await this.ensurePackOwner(params.userId, params.packId);
|
||||
if (pack.type !== 'sticker') {
|
||||
throw new InvalidPackTypeError('sticker');
|
||||
}
|
||||
const stickers = await this.guildRepository.listStickers(pack.id);
|
||||
return await mapGuildStickersWithUsersToResponse(stickers, this.userCacheService, params.requestCache);
|
||||
}
|
||||
|
||||
async createPackEmoji(params: {
|
||||
user: User;
|
||||
packId: GuildID;
|
||||
name: string;
|
||||
image: string;
|
||||
}): Promise<GuildEmojiResponse> {
|
||||
await this.requireExpressionPackAccess(params.user.id);
|
||||
await this.requireFeature(params.user.id, 'feature_global_expressions');
|
||||
const pack = await this.ensurePackOwner(params.user.id, params.packId);
|
||||
if (pack.type !== 'emoji') {
|
||||
throw new InvalidPackTypeError('emoji');
|
||||
}
|
||||
const emojiCount = await this.guildRepository.countEmojis(pack.id);
|
||||
const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS);
|
||||
if (emojiCount >= expressionLimit) {
|
||||
throw new MaxPackExpressionsError(expressionLimit);
|
||||
}
|
||||
const {animated, imageBuffer, contentType} = await this.avatarService.processEmoji({
|
||||
errorPath: 'image',
|
||||
base64Image: params.image,
|
||||
});
|
||||
const emojiId = createEmojiID(await this.apiContext.services.snowflake.generate());
|
||||
await this.avatarService.uploadEmoji({
|
||||
prefix: 'emojis',
|
||||
emojiId,
|
||||
imageBuffer,
|
||||
contentType,
|
||||
});
|
||||
const emoji = await this.guildRepository.upsertEmoji({
|
||||
guild_id: pack.id,
|
||||
emoji_id: emojiId,
|
||||
name: params.name,
|
||||
creator_id: params.user.id,
|
||||
animated,
|
||||
nsfw: null,
|
||||
version: 1,
|
||||
});
|
||||
return mapGuildEmojiToResponse(emoji);
|
||||
}
|
||||
|
||||
async bulkCreatePackEmojis(params: {
|
||||
user: User;
|
||||
packId: GuildID;
|
||||
emojis: Array<{
|
||||
name: string;
|
||||
image: string;
|
||||
}>;
|
||||
}): Promise<{
|
||||
success: Array<GuildEmojiResponse>;
|
||||
failed: Array<{
|
||||
name: string;
|
||||
error: string;
|
||||
}>;
|
||||
}> {
|
||||
await this.requireExpressionPackAccess(params.user.id);
|
||||
await this.requireFeature(params.user.id, 'feature_global_expressions');
|
||||
const pack = await this.ensurePackOwner(params.user.id, params.packId);
|
||||
if (pack.type !== 'emoji') {
|
||||
throw new InvalidPackTypeError('emoji');
|
||||
}
|
||||
let emojiCount = await this.guildRepository.countEmojis(pack.id);
|
||||
const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS);
|
||||
const success: Array<GuildEmojiResponse> = [];
|
||||
const failed: Array<{
|
||||
name: string;
|
||||
error: string;
|
||||
}> = [];
|
||||
for (const emojiData of params.emojis) {
|
||||
if (emojiCount >= expressionLimit) {
|
||||
failed.push({
|
||||
name: emojiData.name,
|
||||
error: this.getLocalizedPackExpressionLimitMessage(params.user.locale, expressionLimit),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const {animated, imageBuffer, contentType} = await this.avatarService.processEmoji({
|
||||
errorPath: `emojis[${success.length + failed.length}].image`,
|
||||
base64Image: emojiData.image,
|
||||
});
|
||||
const emojiId = createEmojiID(await this.apiContext.services.snowflake.generate());
|
||||
await this.avatarService.uploadEmoji({
|
||||
prefix: 'emojis',
|
||||
emojiId,
|
||||
imageBuffer,
|
||||
contentType,
|
||||
});
|
||||
const emoji = await this.guildRepository.upsertEmoji({
|
||||
guild_id: pack.id,
|
||||
emoji_id: emojiId,
|
||||
name: emojiData.name,
|
||||
creator_id: params.user.id,
|
||||
animated,
|
||||
nsfw: null,
|
||||
version: 1,
|
||||
});
|
||||
success.push(mapGuildEmojiToResponse(emoji));
|
||||
emojiCount += 1;
|
||||
} catch (error) {
|
||||
failed.push({
|
||||
name: emojiData.name,
|
||||
error: this.getLocalizedBulkErrorMessage(error, params.user.locale),
|
||||
});
|
||||
}
|
||||
}
|
||||
return {success, failed};
|
||||
}
|
||||
|
||||
async updatePackEmoji(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
emojiId: EmojiID;
|
||||
name: string;
|
||||
}): Promise<GuildEmojiResponse> {
|
||||
await this.requireExpressionPackAccess(params.userId);
|
||||
const pack = await this.ensurePackOwner(params.userId, params.packId);
|
||||
if (pack.type !== 'emoji') {
|
||||
throw new InvalidPackTypeError('emoji');
|
||||
}
|
||||
const emoji = await this.guildRepository.getEmoji(params.emojiId, pack.id);
|
||||
if (!emoji) {
|
||||
throw new UnknownGuildEmojiError();
|
||||
}
|
||||
const updatedEmoji = await this.guildRepository.upsertEmoji({
|
||||
...emoji.toRow(),
|
||||
name: params.name,
|
||||
});
|
||||
return mapGuildEmojiToResponse(updatedEmoji);
|
||||
}
|
||||
|
||||
async deletePackEmoji(params: {userId: UserID; packId: GuildID; emojiId: EmojiID; purge?: boolean}): Promise<void> {
|
||||
await this.requireExpressionPackAccess(params.userId);
|
||||
const pack = await this.ensurePackOwner(params.userId, params.packId);
|
||||
if (pack.type !== 'emoji') {
|
||||
throw new InvalidPackTypeError('emoji');
|
||||
}
|
||||
const emoji = await this.guildRepository.getEmoji(params.emojiId, pack.id);
|
||||
if (!emoji) {
|
||||
throw new UnknownGuildEmojiError();
|
||||
}
|
||||
await this.guildRepository.deleteEmoji(pack.id, params.emojiId);
|
||||
if (params.purge) {
|
||||
await this.assetPurger.purgeEmoji(emoji.id.toString());
|
||||
}
|
||||
}
|
||||
|
||||
async createPackSticker(params: {
|
||||
user: User;
|
||||
packId: GuildID;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
tags: Array<string>;
|
||||
image: string;
|
||||
}): Promise<GuildStickerResponse> {
|
||||
await this.requireExpressionPackAccess(params.user.id);
|
||||
await this.requireFeature(params.user.id, 'feature_global_expressions');
|
||||
const pack = await this.ensurePackOwner(params.user.id, params.packId);
|
||||
if (pack.type !== 'sticker') {
|
||||
throw new InvalidPackTypeError('sticker');
|
||||
}
|
||||
const stickerCount = await this.guildRepository.countStickers(pack.id);
|
||||
const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS);
|
||||
if (stickerCount >= expressionLimit) {
|
||||
throw new MaxPackExpressionsError(expressionLimit);
|
||||
}
|
||||
const {animated, imageBuffer} = await this.avatarService.processSticker({
|
||||
errorPath: 'image',
|
||||
base64Image: params.image,
|
||||
});
|
||||
const stickerId = createStickerID(await this.apiContext.services.snowflake.generate());
|
||||
await this.avatarService.uploadSticker({prefix: 'stickers', stickerId, imageBuffer});
|
||||
const sticker = await this.guildRepository.upsertSticker({
|
||||
guild_id: pack.id,
|
||||
sticker_id: stickerId,
|
||||
name: params.name,
|
||||
description: params.description ?? null,
|
||||
tags: params.tags,
|
||||
animated,
|
||||
nsfw: null,
|
||||
creator_id: params.user.id,
|
||||
version: 1,
|
||||
});
|
||||
const response = mapGuildStickerToResponse(sticker);
|
||||
return response;
|
||||
}
|
||||
|
||||
async bulkCreatePackStickers(params: {
|
||||
user: User;
|
||||
packId: GuildID;
|
||||
stickers: Array<{
|
||||
name: string;
|
||||
description?: string | null;
|
||||
tags: Array<string>;
|
||||
image: string;
|
||||
}>;
|
||||
}): Promise<{
|
||||
success: Array<GuildStickerResponse>;
|
||||
failed: Array<{
|
||||
name: string;
|
||||
error: string;
|
||||
}>;
|
||||
}> {
|
||||
await this.requireExpressionPackAccess(params.user.id);
|
||||
await this.requireFeature(params.user.id, 'feature_global_expressions');
|
||||
const pack = await this.ensurePackOwner(params.user.id, params.packId);
|
||||
if (pack.type !== 'sticker') {
|
||||
throw new InvalidPackTypeError('sticker');
|
||||
}
|
||||
let stickerCount = await this.guildRepository.countStickers(pack.id);
|
||||
const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS);
|
||||
const success: Array<GuildStickerResponse> = [];
|
||||
const failed: Array<{
|
||||
name: string;
|
||||
error: string;
|
||||
}> = [];
|
||||
for (const stickerData of params.stickers) {
|
||||
if (stickerCount >= expressionLimit) {
|
||||
failed.push({
|
||||
name: stickerData.name,
|
||||
error: this.getLocalizedPackExpressionLimitMessage(params.user.locale, expressionLimit),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const {animated, imageBuffer} = await this.avatarService.processSticker({
|
||||
errorPath: `stickers[${success.length + failed.length}].image`,
|
||||
base64Image: stickerData.image,
|
||||
});
|
||||
const stickerId = createStickerID(await this.apiContext.services.snowflake.generate());
|
||||
await this.avatarService.uploadSticker({prefix: 'stickers', stickerId, imageBuffer});
|
||||
const sticker = await this.guildRepository.upsertSticker({
|
||||
guild_id: pack.id,
|
||||
sticker_id: stickerId,
|
||||
name: stickerData.name,
|
||||
description: stickerData.description ?? null,
|
||||
tags: stickerData.tags,
|
||||
animated,
|
||||
nsfw: null,
|
||||
creator_id: params.user.id,
|
||||
version: 1,
|
||||
});
|
||||
success.push(mapGuildStickerToResponse(sticker));
|
||||
stickerCount += 1;
|
||||
} catch (error) {
|
||||
failed.push({
|
||||
name: stickerData.name,
|
||||
error: this.getLocalizedBulkErrorMessage(error, params.user.locale),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const _ of success) {
|
||||
}
|
||||
return {success, failed};
|
||||
}
|
||||
|
||||
async updatePackSticker(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
stickerId: StickerID;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
tags: Array<string>;
|
||||
}): Promise<GuildStickerResponse> {
|
||||
await this.requireExpressionPackAccess(params.userId);
|
||||
const pack = await this.ensurePackOwner(params.userId, params.packId);
|
||||
if (pack.type !== 'sticker') {
|
||||
throw new InvalidPackTypeError('sticker');
|
||||
}
|
||||
const sticker = await this.guildRepository.getSticker(params.stickerId, pack.id);
|
||||
if (!sticker) {
|
||||
throw new UnknownGuildStickerError();
|
||||
}
|
||||
const updatedSticker = await this.guildRepository.upsertSticker({
|
||||
...sticker.toRow(),
|
||||
name: params.name,
|
||||
description: params.description ?? null,
|
||||
tags: params.tags,
|
||||
});
|
||||
return mapGuildStickerToResponse(updatedSticker);
|
||||
}
|
||||
|
||||
async deletePackSticker(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
stickerId: StickerID;
|
||||
purge?: boolean;
|
||||
}): Promise<void> {
|
||||
await this.requireExpressionPackAccess(params.userId);
|
||||
const pack = await this.ensurePackOwner(params.userId, params.packId);
|
||||
if (pack.type !== 'sticker') {
|
||||
throw new InvalidPackTypeError('sticker');
|
||||
}
|
||||
const sticker = await this.guildRepository.getSticker(params.stickerId, pack.id);
|
||||
if (!sticker) {
|
||||
throw new UnknownGuildStickerError();
|
||||
}
|
||||
await this.guildRepository.deleteSticker(pack.id, params.stickerId);
|
||||
if (params.purge) {
|
||||
await this.assetPurger.purgeSticker(sticker.id.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private resolveLimitForUser(user: User | null, key: LimitKey, fallback: number): number {
|
||||
const ctx = createLimitMatchContext({user});
|
||||
return resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, key, fallback);
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {PackIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {
|
||||
PackCreateRequest,
|
||||
PackDashboardResponse,
|
||||
PackSummaryResponse,
|
||||
PackTypeParam,
|
||||
PackUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
import {createGuildID} from '../../BrandedTypes';
|
||||
import {DefaultUserOnly, LoginRequired} from '../../middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '../../middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '../../RateLimitConfig';
|
||||
import type {HonoApp} from '../../types/HonoEnv';
|
||||
import {Validator} from '../../Validator';
|
||||
import {mapPackToSummary} from '../PackModel';
|
||||
|
||||
export function PackController(app: HonoApp) {
|
||||
app.get(
|
||||
'/packs',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_LIST),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
OpenAPI({
|
||||
operationId: 'list_user_packs',
|
||||
summary: 'List user packs',
|
||||
description:
|
||||
'Returns a dashboard view containing all emoji and sticker packs created by or owned by the authenticated user. This includes pack metadata such as name, description, type, and cover image.',
|
||||
responseSchema: PackDashboardResponse,
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const response = await ctx.get('packService').listUserPacks(ctx.get('user').id);
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
'/packs/:pack_type',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_CREATE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', PackTypeParam),
|
||||
Validator('json', PackCreateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'create_pack',
|
||||
summary: 'Create pack',
|
||||
description:
|
||||
'Creates a new emoji or sticker pack owned by the authenticated user. The pack type is specified in the path parameter and can be either "emoji" or "sticker". Returns the newly created pack with its metadata.',
|
||||
responseSchema: PackSummaryResponse,
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const data = ctx.req.valid('json');
|
||||
const pack = await ctx.get('packService').createPack({
|
||||
user,
|
||||
type: ctx.req.valid('param').pack_type as 'emoji' | 'sticker',
|
||||
name: data.name,
|
||||
description: data.description ?? null,
|
||||
});
|
||||
return ctx.json(mapPackToSummary(pack));
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
'/packs/:pack_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_UPDATE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', PackIdParam),
|
||||
Validator('json', PackUpdateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'update_pack',
|
||||
summary: 'Update pack',
|
||||
description:
|
||||
'Updates the metadata for an existing pack owned by the authenticated user. Allowed modifications include name, description, and cover image. Returns the updated pack with all current metadata.',
|
||||
responseSchema: PackSummaryResponse,
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const data = ctx.req.valid('json');
|
||||
const updated = await ctx.get('packService').updatePack({
|
||||
userId: ctx.get('user').id,
|
||||
packId: createGuildID(ctx.req.valid('param').pack_id),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
});
|
||||
return ctx.json(mapPackToSummary(updated));
|
||||
},
|
||||
);
|
||||
app.delete(
|
||||
'/packs/:pack_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_DELETE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', PackIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'delete_pack',
|
||||
summary: 'Delete pack',
|
||||
description:
|
||||
'Permanently deletes a pack owned by the authenticated user along with all emojis or stickers contained within it. This action cannot be undone and will remove all associated assets.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
await ctx.get('packService').deletePack(ctx.get('user').id, createGuildID(ctx.req.valid('param').pack_id));
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
'/packs/:pack_id/install',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_INSTALL),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', PackIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'install_pack',
|
||||
summary: 'Install pack',
|
||||
description:
|
||||
"Installs a pack to the authenticated user's collection, making its emojis or stickers available for use. The pack must be publicly accessible or owned by the user.",
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
await ctx.get('packService').installPack(ctx.get('user').id, createGuildID(ctx.req.valid('param').pack_id));
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
app.delete(
|
||||
'/packs/:pack_id/install',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_INSTALL),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', PackIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'uninstall_pack',
|
||||
summary: 'Uninstall pack',
|
||||
description:
|
||||
"Uninstalls a pack from the authenticated user's collection, removing access to its emojis or stickers. This does not delete the pack itself, only removes it from the user's installed list.",
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
await ctx.get('packService').uninstallPack(ctx.get('user').id, createGuildID(ctx.req.valid('param').pack_id));
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {PackIdEmojiIdParam, PackIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {PurgeQuery} from '@fluxer/schema/src/domains/common/CommonQuerySchemas';
|
||||
import {
|
||||
GuildEmojiBulkCreateResponse,
|
||||
GuildEmojiResponse,
|
||||
GuildEmojiWithUserListResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
import {
|
||||
GuildEmojiBulkCreateRequest,
|
||||
GuildEmojiCreateRequest,
|
||||
GuildEmojiUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
|
||||
import {createEmojiID, createGuildID} from '../../BrandedTypes';
|
||||
import {LoginRequired} from '../../middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '../../middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '../../RateLimitConfig';
|
||||
import type {HonoApp} from '../../types/HonoEnv';
|
||||
import {Validator} from '../../Validator';
|
||||
|
||||
export function PackEmojiController(app: HonoApp) {
|
||||
app.post(
|
||||
'/packs/emojis/:pack_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJI_CREATE),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdParam),
|
||||
Validator('json', GuildEmojiCreateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'create_pack_emoji',
|
||||
summary: 'Create pack emoji',
|
||||
description:
|
||||
'Creates a new emoji within the specified pack. Requires the pack ID in the path and emoji metadata (name and image data) in the request body. Returns the newly created emoji with its generated ID.',
|
||||
responseSchema: GuildEmojiResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const packId = createGuildID(ctx.req.valid('param').pack_id);
|
||||
const {name, image} = ctx.req.valid('json');
|
||||
const user = ctx.get('user');
|
||||
return ctx.json(await ctx.get('packService').createPackEmoji({user, packId, name, image}));
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
'/packs/emojis/:pack_id/bulk',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJI_BULK_CREATE),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdParam),
|
||||
Validator('json', GuildEmojiBulkCreateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'bulk_create_pack_emojis',
|
||||
summary: 'Bulk create pack emojis',
|
||||
description:
|
||||
'Creates multiple emojis within the specified pack in a single bulk operation. Accepts an array of emoji definitions, each containing name and image data. Returns a response containing all successfully created emojis.',
|
||||
responseSchema: GuildEmojiBulkCreateResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const packId = createGuildID(ctx.req.valid('param').pack_id);
|
||||
const {emojis} = ctx.req.valid('json');
|
||||
const user = ctx.get('user');
|
||||
return ctx.json(await ctx.get('packService').bulkCreatePackEmojis({user, packId, emojis}));
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
'/packs/emojis/:pack_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJIS_LIST),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'list_pack_emojis',
|
||||
summary: 'List pack emojis',
|
||||
description:
|
||||
'Returns a list of all emojis contained within the specified pack, including emoji metadata and creator information. Results include emoji ID, name, image URL, and the user who created each emoji.',
|
||||
responseSchema: GuildEmojiWithUserListResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const packId = createGuildID(ctx.req.valid('param').pack_id);
|
||||
const userId = ctx.get('user').id;
|
||||
const requestCache = ctx.get('requestCache');
|
||||
return ctx.json(await ctx.get('packService').getPackEmojis({userId, packId, requestCache}));
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
'/packs/emojis/:pack_id/:emoji_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJI_UPDATE),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdEmojiIdParam),
|
||||
Validator('json', GuildEmojiUpdateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'update_pack_emoji',
|
||||
summary: 'Update pack emoji',
|
||||
description:
|
||||
'Updates the name of an existing emoji within the specified pack. Requires both pack ID and emoji ID in the path parameters. Returns the updated emoji with its new name and all existing metadata.',
|
||||
responseSchema: GuildEmojiResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {pack_id, emoji_id} = ctx.req.valid('param');
|
||||
const packId = createGuildID(pack_id);
|
||||
const emojiId = createEmojiID(emoji_id);
|
||||
const {name} = ctx.req.valid('json');
|
||||
return ctx.json(
|
||||
await ctx.get('packService').updatePackEmoji({userId: ctx.get('user').id, packId, emojiId, name}),
|
||||
);
|
||||
},
|
||||
);
|
||||
app.delete(
|
||||
'/packs/emojis/:pack_id/:emoji_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJI_DELETE),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdEmojiIdParam),
|
||||
Validator('query', PurgeQuery),
|
||||
OpenAPI({
|
||||
operationId: 'delete_pack_emoji',
|
||||
summary: 'Delete pack emoji',
|
||||
description:
|
||||
'Permanently deletes an emoji from the specified pack. Requires both pack ID and emoji ID in the path parameters. Accepts an optional "purge" query parameter to control whether associated assets are immediately deleted.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {pack_id, emoji_id} = ctx.req.valid('param');
|
||||
const packId = createGuildID(pack_id);
|
||||
const emojiId = createEmojiID(emoji_id);
|
||||
const purge = ctx.req.valid('query').purge ?? false;
|
||||
await ctx.get('packService').deletePackEmoji({
|
||||
userId: ctx.get('user').id,
|
||||
packId,
|
||||
emojiId,
|
||||
purge,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {PackIdParam, PackIdStickerIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {PurgeQuery} from '@fluxer/schema/src/domains/common/CommonQuerySchemas';
|
||||
import {
|
||||
GuildStickerBulkCreateResponse,
|
||||
GuildStickerResponse,
|
||||
GuildStickerWithUserListResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
import {
|
||||
GuildStickerBulkCreateRequest,
|
||||
GuildStickerCreateRequest,
|
||||
GuildStickerUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
|
||||
import {createGuildID, createStickerID} from '../../BrandedTypes';
|
||||
import {LoginRequired} from '../../middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '../../middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '../../RateLimitConfig';
|
||||
import type {HonoApp} from '../../types/HonoEnv';
|
||||
import {Validator} from '../../Validator';
|
||||
|
||||
export function PackStickerController(app: HonoApp) {
|
||||
app.post(
|
||||
'/packs/stickers/:pack_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_STICKER_CREATE),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdParam),
|
||||
Validator('json', GuildStickerCreateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'create_pack_sticker',
|
||||
summary: 'Create pack sticker',
|
||||
description:
|
||||
'Creates a new sticker within the specified pack. Requires the pack ID in the path and sticker metadata (name, description, tags, and image data) in the request body. Returns the newly created sticker with its generated ID.',
|
||||
responseSchema: GuildStickerResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const packId = createGuildID(ctx.req.valid('param').pack_id);
|
||||
const {name, description, tags, image} = ctx.req.valid('json');
|
||||
const user = ctx.get('user');
|
||||
const sticker = await ctx.get('packService').createPackSticker({user, packId, name, description, tags, image});
|
||||
return ctx.json(sticker);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
'/packs/stickers/:pack_id/bulk',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_STICKER_BULK_CREATE),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdParam),
|
||||
Validator('json', GuildStickerBulkCreateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'bulk_create_pack_stickers',
|
||||
summary: 'Bulk create pack stickers',
|
||||
description:
|
||||
'Creates multiple stickers within the specified pack in a single bulk operation. Accepts an array of sticker definitions, each containing name, description, tags, and image data. Returns a response containing all successfully created stickers.',
|
||||
responseSchema: GuildStickerBulkCreateResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const packId = createGuildID(ctx.req.valid('param').pack_id);
|
||||
const {stickers} = ctx.req.valid('json');
|
||||
const user = ctx.get('user');
|
||||
const result = await ctx.get('packService').bulkCreatePackStickers({user, packId, stickers});
|
||||
return ctx.json(result);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
'/packs/stickers/:pack_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_STICKERS_LIST),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'list_pack_stickers',
|
||||
summary: 'List pack stickers',
|
||||
description:
|
||||
'Returns a list of all stickers contained within the specified pack, including sticker metadata and creator information. Results include sticker ID, name, description, tags, image URL, and the user who created each sticker.',
|
||||
responseSchema: GuildStickerWithUserListResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const packId = createGuildID(ctx.req.valid('param').pack_id);
|
||||
const userId = ctx.get('user').id;
|
||||
const requestCache = ctx.get('requestCache');
|
||||
return ctx.json(await ctx.get('packService').getPackStickers({userId, packId, requestCache}));
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
'/packs/stickers/:pack_id/:sticker_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_STICKER_UPDATE),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdStickerIdParam),
|
||||
Validator('json', GuildStickerUpdateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'update_pack_sticker',
|
||||
summary: 'Update pack sticker',
|
||||
description:
|
||||
'Updates the name, description, or tags of an existing sticker within the specified pack. Requires both pack ID and sticker ID in the path parameters. Returns the updated sticker with its new metadata and all existing fields.',
|
||||
responseSchema: GuildStickerResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {pack_id, sticker_id} = ctx.req.valid('param');
|
||||
const packId = createGuildID(pack_id);
|
||||
const stickerId = createStickerID(sticker_id);
|
||||
const {name, description, tags} = ctx.req.valid('json');
|
||||
return ctx.json(
|
||||
await ctx
|
||||
.get('packService')
|
||||
.updatePackSticker({userId: ctx.get('user').id, packId, stickerId, name, description, tags}),
|
||||
);
|
||||
},
|
||||
);
|
||||
app.delete(
|
||||
'/packs/stickers/:pack_id/:sticker_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.PACKS_STICKER_DELETE),
|
||||
LoginRequired,
|
||||
Validator('param', PackIdStickerIdParam),
|
||||
Validator('query', PurgeQuery),
|
||||
OpenAPI({
|
||||
operationId: 'delete_pack_sticker',
|
||||
summary: 'Delete pack sticker',
|
||||
description:
|
||||
'Permanently deletes a sticker from the specified pack. Requires both pack ID and sticker ID in the path parameters. Accepts an optional "purge" query parameter to control whether associated assets are immediately deleted.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {pack_id, sticker_id} = ctx.req.valid('param');
|
||||
const packId = createGuildID(pack_id);
|
||||
const stickerId = createStickerID(sticker_id);
|
||||
const {purge = false} = ctx.req.valid('query');
|
||||
await ctx.get('packService').deletePackSticker({
|
||||
userId: ctx.get('user').id,
|
||||
packId,
|
||||
stickerId,
|
||||
purge,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {HonoApp} from '../../types/HonoEnv';
|
||||
import {PackController} from './PackController';
|
||||
import {PackEmojiController} from './PackEmojiController';
|
||||
import {PackStickerController} from './PackStickerController';
|
||||
|
||||
export function registerPackControllers(app: HonoApp) {
|
||||
PackController(app);
|
||||
PackEmojiController(app);
|
||||
PackStickerController(app);
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '../../test/TestConstants';
|
||||
import {createBuilder} from '../../test/TestRequestBuilder';
|
||||
import {
|
||||
createPack,
|
||||
createPackEmoji,
|
||||
createPackSticker,
|
||||
deletePack,
|
||||
deletePackEmoji,
|
||||
deletePackSticker,
|
||||
getPackEmojis,
|
||||
getPackStickers,
|
||||
installPack,
|
||||
listPacks,
|
||||
setupPackTestAccount,
|
||||
uninstallPack,
|
||||
updatePack,
|
||||
} from './PackTestUtils';
|
||||
|
||||
describe('Pack Invite Flow', () => {
|
||||
let harness: ApiTestHarness;
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
test('user can create and list emoji pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {
|
||||
name: 'My Emoji Pack',
|
||||
description: 'A collection of emojis',
|
||||
});
|
||||
const dashboard = await listPacks(harness, account.token);
|
||||
const createdPack = dashboard.emoji.created.find((p) => p.id === pack.id);
|
||||
expect(createdPack).toBeTruthy();
|
||||
expect(createdPack?.name).toBe('My Emoji Pack');
|
||||
});
|
||||
test('user can update pack name and description', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'Original Name'});
|
||||
const updated = await updatePack(harness, account.token, pack.id, {
|
||||
name: 'Updated Name',
|
||||
description: 'New description',
|
||||
});
|
||||
expect(updated.name).toBe('Updated Name');
|
||||
});
|
||||
test('user can delete own pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'To Delete'});
|
||||
await deletePack(harness, account.token, pack.id);
|
||||
const dashboard = await listPacks(harness, account.token);
|
||||
const deletedPack = dashboard.emoji.created.find((p) => p.id === pack.id);
|
||||
expect(deletedPack).toBeUndefined();
|
||||
});
|
||||
test('user cannot update another users pack', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Owners Pack'});
|
||||
const {account: other} = await setupPackTestAccount(harness);
|
||||
await createBuilder(harness, other.token)
|
||||
.patch(`/packs/${pack.id}`)
|
||||
.body({name: 'Stolen Name'})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
test('user cannot delete another users pack', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Protected Pack'});
|
||||
const {account: other} = await setupPackTestAccount(harness);
|
||||
await createBuilder(harness, other.token).delete(`/packs/${pack.id}`).expect(HTTP_STATUS.FORBIDDEN).execute();
|
||||
});
|
||||
test('user can install pack from another user', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Shareable Pack'});
|
||||
const {account: installer} = await setupPackTestAccount(harness);
|
||||
await installPack(harness, installer.token, pack.id);
|
||||
const dashboard = await listPacks(harness, installer.token);
|
||||
expect(dashboard.emoji.installed.some((p) => p.id === pack.id)).toBe(true);
|
||||
});
|
||||
test('uninstall pack returns success', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Uninstall Test Pack'});
|
||||
const {account: installer} = await setupPackTestAccount(harness);
|
||||
await installPack(harness, installer.token, pack.id);
|
||||
await uninstallPack(harness, installer.token, pack.id);
|
||||
});
|
||||
test('installing pack is idempotent', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Idempotent Pack'});
|
||||
const {account: installer} = await setupPackTestAccount(harness);
|
||||
await installPack(harness, installer.token, pack.id);
|
||||
await installPack(harness, installer.token, pack.id);
|
||||
const dashboard = await listPacks(harness, installer.token);
|
||||
const installedCount = dashboard.emoji.installed.filter((p) => p.id === pack.id).length;
|
||||
expect(installedCount).toBe(1);
|
||||
});
|
||||
test('cannot install non-existent pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
await createBuilder(harness, account.token)
|
||||
.post('/packs/999999999999999999/install')
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
test('can add emoji to pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'Emoji Pack'});
|
||||
const emoji = await createPackEmoji(harness, account.token, pack.id, 'test_emoji');
|
||||
expect(emoji.id).toBeTruthy();
|
||||
expect(emoji.name).toBe('test_emoji');
|
||||
const emojis = await getPackEmojis(harness, account.token, pack.id);
|
||||
expect(emojis.some((e) => e.id === emoji.id)).toBe(true);
|
||||
});
|
||||
test('can add sticker to pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'sticker', {name: 'Sticker Pack'});
|
||||
const sticker = await createPackSticker(harness, account.token, pack.id, 'test_sticker', ['happy', 'fun']);
|
||||
expect(sticker.id).toBeTruthy();
|
||||
expect(sticker.name).toBe('test_sticker');
|
||||
const stickers = await getPackStickers(harness, account.token, pack.id);
|
||||
expect(stickers.some((s) => s.id === sticker.id)).toBe(true);
|
||||
});
|
||||
test('cannot add emoji to sticker pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'sticker', {name: 'Wrong Type Pack'});
|
||||
await createBuilder(harness, account.token)
|
||||
.post(`/packs/emojis/${pack.id}`)
|
||||
.body({name: 'wrong_emoji', image: 'data:image/png;base64,iVBORw0KGgo='})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
test('cannot add sticker to emoji pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'Wrong Type Pack'});
|
||||
await createBuilder(harness, account.token)
|
||||
.post(`/packs/stickers/${pack.id}`)
|
||||
.body({name: 'wrong_sticker', tags: ['test'], image: 'data:image/png;base64,iVBORw0KGgo='})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
test('can delete emoji from pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'Delete Emoji Pack'});
|
||||
const emoji = await createPackEmoji(harness, account.token, pack.id, 'to_delete');
|
||||
await deletePackEmoji(harness, account.token, pack.id, emoji.id);
|
||||
const emojis = await getPackEmojis(harness, account.token, pack.id);
|
||||
expect(emojis.some((e) => e.id === emoji.id)).toBe(false);
|
||||
});
|
||||
test('can delete sticker from pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'sticker', {name: 'Delete Sticker Pack'});
|
||||
const sticker = await createPackSticker(harness, account.token, pack.id, 'to_delete', ['bye']);
|
||||
await deletePackSticker(harness, account.token, pack.id, sticker.id);
|
||||
const stickers = await getPackStickers(harness, account.token, pack.id);
|
||||
expect(stickers.some((s) => s.id === sticker.id)).toBe(false);
|
||||
});
|
||||
test('cannot add emoji to another users pack', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Protected Emoji Pack'});
|
||||
const {account: other} = await setupPackTestAccount(harness);
|
||||
await createBuilder(harness, other.token)
|
||||
.post(`/packs/emojis/${pack.id}`)
|
||||
.body({name: 'unauthorized', image: 'data:image/png;base64,iVBORw0KGgo='})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
test('pack creator shows in created packs list', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'My Created Pack'});
|
||||
const dashboard = await listPacks(harness, account.token);
|
||||
expect(dashboard.emoji.created.some((p) => p.id === pack.id)).toBe(true);
|
||||
expect(dashboard.emoji.installed.some((p) => p.id === pack.id)).toBe(false);
|
||||
});
|
||||
test('multiple packs can be created and managed', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const emojiPack1 = await createPack(harness, account.token, 'emoji', {name: 'Emoji Pack 1'});
|
||||
const emojiPack2 = await createPack(harness, account.token, 'emoji', {name: 'Emoji Pack 2'});
|
||||
const stickerPack = await createPack(harness, account.token, 'sticker', {name: 'Sticker Pack 1'});
|
||||
const dashboard = await listPacks(harness, account.token);
|
||||
expect(dashboard.emoji.created.length).toBe(2);
|
||||
expect(dashboard.sticker.created.length).toBe(1);
|
||||
expect(dashboard.emoji.created.some((p) => p.id === emojiPack1.id)).toBe(true);
|
||||
expect(dashboard.emoji.created.some((p) => p.id === emojiPack2.id)).toBe(true);
|
||||
expect(dashboard.sticker.created.some((p) => p.id === stickerPack.id)).toBe(true);
|
||||
});
|
||||
test('installed pack shows installed_at timestamp', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Timestamped Pack'});
|
||||
const {account: installer} = await setupPackTestAccount(harness);
|
||||
await installPack(harness, installer.token, pack.id);
|
||||
const dashboard = await listPacks(harness, installer.token);
|
||||
const installedPack = dashboard.emoji.installed.find((p) => p.id === pack.id);
|
||||
expect(installedPack).toBeTruthy();
|
||||
expect(installedPack?.installed_at).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '../../test/TestConstants';
|
||||
import {createBuilder} from '../../test/TestRequestBuilder';
|
||||
import {
|
||||
createPack,
|
||||
grantPremium,
|
||||
grantStaffAccess,
|
||||
installPack,
|
||||
listPacks,
|
||||
revokePremium,
|
||||
setupNonPremiumPackTestAccount,
|
||||
setupPackTestAccount,
|
||||
} from './PackTestUtils';
|
||||
|
||||
describe('Pack Premium Requirements', () => {
|
||||
let harness: ApiTestHarness;
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
test('user without staff flag cannot list packs', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
await createBuilder(harness, account.token).get('/packs').expect(HTTP_STATUS.FORBIDDEN).execute();
|
||||
});
|
||||
test('user with staff flag but no premium cannot create pack', async () => {
|
||||
const {account} = await setupNonPremiumPackTestAccount(harness);
|
||||
await createBuilder(harness, account.token)
|
||||
.post('/packs/emoji')
|
||||
.body({name: 'Test Pack'})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
test('premium user with staff flag can create emoji pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'My Emoji Pack'});
|
||||
expect(pack.id).toBeTruthy();
|
||||
expect(pack.name).toBe('My Emoji Pack');
|
||||
expect(pack.type).toBe('emoji');
|
||||
});
|
||||
test('premium user with staff flag can create sticker pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'sticker', {name: 'My Sticker Pack'});
|
||||
expect(pack.id).toBeTruthy();
|
||||
expect(pack.name).toBe('My Sticker Pack');
|
||||
expect(pack.type).toBe('sticker');
|
||||
});
|
||||
test('non-premium user cannot create pack emoji', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'Emoji Pack'});
|
||||
await revokePremium(harness, account.userId);
|
||||
await createBuilder(harness, account.token)
|
||||
.post(`/packs/emojis/${pack.id}`)
|
||||
.body({name: 'emoji1', image: 'data:image/png;base64,iVBORw0KGgo='})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
test('non-premium user cannot install pack', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Shared Pack'});
|
||||
const {account: installer} = await setupNonPremiumPackTestAccount(harness);
|
||||
await createBuilder(harness, installer.token)
|
||||
.post(`/packs/${pack.id}/install`)
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
test('premium user can install pack', async () => {
|
||||
const {account: owner} = await setupPackTestAccount(harness);
|
||||
const pack = await createPack(harness, owner.token, 'emoji', {name: 'Installable Pack'});
|
||||
const {account: installer} = await setupPackTestAccount(harness);
|
||||
await installPack(harness, installer.token, pack.id);
|
||||
const dashboard = await listPacks(harness, installer.token);
|
||||
const installed = dashboard.emoji.installed.find((p) => p.id === pack.id);
|
||||
expect(installed).toBeTruthy();
|
||||
});
|
||||
test('user without staff flag cannot access pack endpoints', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
await createBuilder(harness, account.token).get('/packs').expect(HTTP_STATUS.FORBIDDEN).execute();
|
||||
await createBuilder(harness, account.token)
|
||||
.post('/packs/emoji')
|
||||
.body({name: 'Unauthorized Pack'})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
test('user gains staff flag and can access expression packs', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
await grantPremium(harness, owner.userId);
|
||||
const dashboard = await listPacks(harness, owner.token);
|
||||
expect(dashboard.emoji).toBeTruthy();
|
||||
expect(dashboard.sticker).toBeTruthy();
|
||||
});
|
||||
test('pack limits show correct values for premium user', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
const dashboard = await listPacks(harness, account.token);
|
||||
expect(dashboard.emoji.created_limit).toBeGreaterThan(0);
|
||||
expect(dashboard.emoji.installed_limit).toBeGreaterThan(0);
|
||||
expect(dashboard.sticker.created_limit).toBeGreaterThan(0);
|
||||
expect(dashboard.sticker.installed_limit).toBeGreaterThan(0);
|
||||
});
|
||||
test('pack limits show zero for non-premium user with staff flag', async () => {
|
||||
const {account} = await setupNonPremiumPackTestAccount(harness);
|
||||
const dashboard = await listPacks(harness, account.token);
|
||||
expect(dashboard.emoji.created_limit).toBe(0);
|
||||
expect(dashboard.emoji.installed_limit).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,206 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {join} from 'node:path';
|
||||
import type {
|
||||
GuildEmojiResponse,
|
||||
GuildEmojiWithUserResponse,
|
||||
GuildStickerResponse,
|
||||
GuildStickerWithUserResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import type {PackDashboardResponse, PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
import {createTestAccount, type TestAccount} from '../../auth/tests/AuthTestUtils';
|
||||
import type {ApiTestHarness} from '../../test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '../../test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '../../test/TestRequestBuilder';
|
||||
|
||||
interface PackCreateRequest {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
interface PackUpdateRequest {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
type PackType = 'emoji' | 'sticker';
|
||||
|
||||
function loadPackFixture(filename: string): Buffer {
|
||||
const fixturesPath = join(import.meta.dirname, '..', '..', 'test', 'fixtures', filename);
|
||||
return readFileSync(fixturesPath);
|
||||
}
|
||||
|
||||
async function createGuild(harness: ApiTestHarness, token: string, name: string): Promise<GuildResponse> {
|
||||
return createBuilder<GuildResponse>(harness, token).post('/guilds').body({name}).expect(HTTP_STATUS.OK).execute();
|
||||
}
|
||||
|
||||
export async function grantStaffAccess(harness: ApiTestHarness, userId: string): Promise<void> {
|
||||
await createBuilderWithoutAuth(harness).patch(`/test/users/${userId}/flags`).body({flags: 1}).execute();
|
||||
}
|
||||
|
||||
export async function grantPremium(harness: ApiTestHarness, userId: string): Promise<void> {
|
||||
const premiumUntil = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/test/users/${userId}/premium`)
|
||||
.body({
|
||||
premium_type: 2,
|
||||
premium_until: premiumUntil,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function revokePremium(harness: ApiTestHarness, userId: string): Promise<void> {
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/test/users/${userId}/premium`)
|
||||
.body({
|
||||
premium_type: null,
|
||||
premium_until: null,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function setupPackTestAccount(harness: ApiTestHarness): Promise<{
|
||||
account: TestAccount;
|
||||
guild: GuildResponse;
|
||||
}> {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Pack Test Guild');
|
||||
await grantStaffAccess(harness, account.userId);
|
||||
await grantPremium(harness, account.userId);
|
||||
return {account, guild};
|
||||
}
|
||||
|
||||
export async function setupNonPremiumPackTestAccount(harness: ApiTestHarness): Promise<{
|
||||
account: TestAccount;
|
||||
guild: GuildResponse;
|
||||
}> {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Pack Test Guild');
|
||||
await grantStaffAccess(harness, account.userId);
|
||||
return {account, guild};
|
||||
}
|
||||
|
||||
export async function listPacks(harness: ApiTestHarness, token: string): Promise<PackDashboardResponse> {
|
||||
return createBuilder<PackDashboardResponse>(harness, token).get('/packs').expect(HTTP_STATUS.OK).execute();
|
||||
}
|
||||
|
||||
export async function createPack(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packType: PackType,
|
||||
data: PackCreateRequest,
|
||||
): Promise<PackSummaryResponse> {
|
||||
return createBuilder<PackSummaryResponse>(harness, token)
|
||||
.post(`/packs/${packType}`)
|
||||
.body(data)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function updatePack(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
data: PackUpdateRequest,
|
||||
): Promise<PackSummaryResponse> {
|
||||
return createBuilder<PackSummaryResponse>(harness, token)
|
||||
.patch(`/packs/${packId}`)
|
||||
.body(data)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function deletePack(harness: ApiTestHarness, token: string, packId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, token).delete(`/packs/${packId}`).expect(HTTP_STATUS.NO_CONTENT).execute();
|
||||
}
|
||||
|
||||
export async function installPack(harness: ApiTestHarness, token: string, packId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, token)
|
||||
.post(`/packs/${packId}/install`)
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function uninstallPack(harness: ApiTestHarness, token: string, packId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, token).delete(`/packs/${packId}/install`).expect(HTTP_STATUS.NO_CONTENT).execute();
|
||||
}
|
||||
|
||||
export async function getPackEmojis(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
): Promise<Array<GuildEmojiWithUserResponse>> {
|
||||
return createBuilder<Array<GuildEmojiWithUserResponse>>(harness, token)
|
||||
.get(`/packs/emojis/${packId}`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function getPackStickers(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
): Promise<Array<GuildStickerWithUserResponse>> {
|
||||
return createBuilder<Array<GuildStickerWithUserResponse>>(harness, token)
|
||||
.get(`/packs/stickers/${packId}`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function createPackEmoji(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
name: string,
|
||||
imageBase64?: string,
|
||||
): Promise<GuildEmojiResponse> {
|
||||
const image = imageBase64 ?? loadPackFixture('yeah.png').toString('base64');
|
||||
return createBuilder<GuildEmojiResponse>(harness, token)
|
||||
.post(`/packs/emojis/${packId}`)
|
||||
.body({name, image: `data:image/png;base64,${image}`})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function deletePackEmoji(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
emojiId: string,
|
||||
): Promise<void> {
|
||||
await createBuilder<void>(harness, token)
|
||||
.delete(`/packs/emojis/${packId}/${emojiId}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function createPackSticker(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
name: string,
|
||||
tags: Array<string>,
|
||||
imageBase64?: string,
|
||||
): Promise<GuildStickerResponse> {
|
||||
const image = imageBase64 ?? loadPackFixture('sticker.png').toString('base64');
|
||||
return createBuilder<GuildStickerResponse>(harness, token)
|
||||
.post(`/packs/stickers/${packId}`)
|
||||
.body({name, tags, image: `data:image/png;base64,${image}`})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function deletePackSticker(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
stickerId: string,
|
||||
): Promise<void> {
|
||||
await createBuilder<void>(harness, token)
|
||||
.delete(`/packs/stickers/${packId}/${stickerId}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ms} from 'itty-time';
|
||||
import type {RouteRateLimitConfig} from '../middleware/RateLimitMiddleware';
|
||||
|
||||
export const PackRateLimitConfigs = {
|
||||
PACKS_LIST: {
|
||||
bucket: 'packs:list',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_CREATE: {
|
||||
bucket: 'packs:create',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_UPDATE: {
|
||||
bucket: 'packs:update::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_DELETE: {
|
||||
bucket: 'packs:delete::pack_id',
|
||||
config: {limit: 10, windowMs: ms('1 minute')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_INSTALL: {
|
||||
bucket: 'packs:install::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_INVITES_LIST: {
|
||||
bucket: 'packs:invite:list::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_INVITES_CREATE: {
|
||||
bucket: 'packs:invite:create::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_EMOJIS_LIST: {
|
||||
bucket: 'packs:emoji:list::pack_id',
|
||||
config: {limit: 60, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_EMOJI_CREATE: {
|
||||
bucket: 'packs:emoji:create::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_EMOJI_BULK_CREATE: {
|
||||
bucket: 'packs:emoji:bulk_create::pack_id',
|
||||
config: {limit: 6, windowMs: ms('1 minute')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_EMOJI_UPDATE: {
|
||||
bucket: 'packs:emoji:update::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_EMOJI_DELETE: {
|
||||
bucket: 'packs:emoji:delete::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_STICKERS_LIST: {
|
||||
bucket: 'packs:sticker:list::pack_id',
|
||||
config: {limit: 60, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_STICKER_CREATE: {
|
||||
bucket: 'packs:sticker:create::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_STICKER_BULK_CREATE: {
|
||||
bucket: 'packs:sticker:bulk_create::pack_id',
|
||||
config: {limit: 6, windowMs: ms('1 minute')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_STICKER_UPDATE: {
|
||||
bucket: 'packs:sticker:update::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
PACKS_STICKER_DELETE: {
|
||||
bucket: 'packs:sticker:delete::pack_id',
|
||||
config: {limit: 20, windowMs: ms('10 seconds')},
|
||||
} as RouteRateLimitConfig,
|
||||
} as const;
|
||||
@@ -56,8 +56,6 @@ import type {OAuth2RequestService} from '../oauth/OAuth2RequestService';
|
||||
import type {OAuth2Service} from '../oauth/OAuth2Service';
|
||||
import type {IApplicationRepository} from '../oauth/repositories/IApplicationRepository';
|
||||
import type {IOAuth2TokenRepository} from '../oauth/repositories/IOAuth2TokenRepository';
|
||||
import type {PackRepository} from '../pack/PackRepository';
|
||||
import type {PackService} from '../pack/PackService';
|
||||
import type {ReadStateRequestService} from '../read_state/ReadStateRequestService';
|
||||
import type {ReadStateService} from '../read_state/ReadStateService';
|
||||
import type {ReportRequestService} from '../report/ReportRequestService';
|
||||
@@ -142,8 +140,6 @@ export interface HonoEnv {
|
||||
gatewayRequestService: GatewayRequestService;
|
||||
discoveryService: IGuildDiscoveryService;
|
||||
guildService: GuildService;
|
||||
packService: PackService;
|
||||
packRepository: PackRepository;
|
||||
inviteService: InviteService;
|
||||
inviteRequestService: InviteRequestService;
|
||||
liveKitWebhookService?: LiveKitWebhookService;
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('User custom status emoji premium', () => {
|
||||
expect(settings.custom_status?.emoji_id).toBeUndefined();
|
||||
expect(settings.custom_status?.emoji_animated).toBe(false);
|
||||
});
|
||||
it('allows custom emoji status for premium users without guild or installed pack access', async () => {
|
||||
it('allows custom emoji status for premium users without guild access', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const premiumAccount = await createTestAccount(harness);
|
||||
await grantPremium(harness, premiumAccount.userId, UserPremiumTypes.SUBSCRIPTION);
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {LimitConfigService} from '../limits/LimitConfigService';
|
||||
import {resolveLimitSafe} from '../limits/LimitConfigUtils';
|
||||
import {createLimitMatchContext} from '../limits/LimitMatchContextBuilder';
|
||||
import type {GuildEmoji} from '../models/GuildEmoji';
|
||||
import type {PackExpressionAccessResolution, PackExpressionAccessResolver} from '../pack/PackExpressionAccessResolver';
|
||||
import type {IUserAccountRepository} from '../user/repositories/IUserAccountRepository';
|
||||
|
||||
type EmojiGuildRepository = Pick<IGuildRepositoryAggregate, 'getEmoji' | 'getEmojiById'>;
|
||||
@@ -26,7 +25,6 @@ interface SanitizeCustomEmojisParams {
|
||||
guildRepository: EmojiGuildRepository;
|
||||
limitConfigService: LimitConfigService;
|
||||
hasPermission?: (permission: bigint) => Promise<boolean>;
|
||||
packResolver?: PackExpressionAccessResolver;
|
||||
}
|
||||
|
||||
interface EmojiMatch {
|
||||
@@ -43,17 +41,8 @@ interface CodeBlock {
|
||||
}
|
||||
|
||||
export async function sanitizeCustomEmojis(params: SanitizeCustomEmojisParams): Promise<string> {
|
||||
const {
|
||||
content,
|
||||
userId,
|
||||
webhookId,
|
||||
guildId,
|
||||
userRepository,
|
||||
guildRepository,
|
||||
limitConfigService,
|
||||
hasPermission,
|
||||
packResolver,
|
||||
} = params;
|
||||
const {content, userId, webhookId, guildId, userRepository, guildRepository, limitConfigService, hasPermission} =
|
||||
params;
|
||||
const escapedContexts = parseEscapedContexts(content);
|
||||
const isInEscapedContext = (index: number): boolean =>
|
||||
escapedContexts.some((ctx) => index >= ctx.start && index < ctx.end);
|
||||
@@ -80,7 +69,6 @@ export async function sanitizeCustomEmojis(params: SanitizeCustomEmojisParams):
|
||||
isWebhook,
|
||||
hasGlobalExpressions,
|
||||
canUseExternalEmojis,
|
||||
packResolver,
|
||||
});
|
||||
return applyReplacements(content, replacements);
|
||||
}
|
||||
@@ -171,7 +159,6 @@ async function determineReplacements(params: {
|
||||
isWebhook: boolean;
|
||||
hasGlobalExpressions: number;
|
||||
canUseExternalEmojis: boolean | null;
|
||||
packResolver?: PackExpressionAccessResolver;
|
||||
}): Promise<Array<Replacement>> {
|
||||
const {emojiMatches, emojiLookups, guildId, isWebhook, hasGlobalExpressions, canUseExternalEmojis} = params;
|
||||
const replacements: Array<Replacement> = [];
|
||||
@@ -184,7 +171,6 @@ async function determineReplacements(params: {
|
||||
isWebhook,
|
||||
hasGlobalExpressions,
|
||||
canUseExternalEmojis,
|
||||
packResolver: params.packResolver,
|
||||
});
|
||||
if (shouldReplace) {
|
||||
replacements.push({
|
||||
@@ -203,9 +189,8 @@ async function shouldReplaceEmoji(params: {
|
||||
isWebhook: boolean;
|
||||
hasGlobalExpressions: number;
|
||||
canUseExternalEmojis: boolean | null;
|
||||
packResolver?: PackExpressionAccessResolver;
|
||||
}): Promise<boolean> {
|
||||
const {lookup, guildId, isWebhook, hasGlobalExpressions, canUseExternalEmojis, packResolver} = params;
|
||||
const {lookup, guildId, isWebhook, hasGlobalExpressions, canUseExternalEmojis} = params;
|
||||
if (!guildId) {
|
||||
if (!lookup.globalEmoji) return true;
|
||||
if (!isWebhook && hasGlobalExpressions === 0) return true;
|
||||
@@ -215,23 +200,11 @@ async function shouldReplaceEmoji(params: {
|
||||
return false;
|
||||
}
|
||||
if (!lookup.globalEmoji) return true;
|
||||
const packAccess = await resolvePackAccessStatus(lookup.globalEmoji.guildId, packResolver);
|
||||
if (packAccess === 'not-accessible') {
|
||||
return true;
|
||||
}
|
||||
if (!isWebhook && hasGlobalExpressions === 0) return true;
|
||||
if (hasGlobalExpressions > 0 && canUseExternalEmojis === false) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function resolvePackAccessStatus(
|
||||
packId: GuildID,
|
||||
packResolver?: PackExpressionAccessResolver,
|
||||
): Promise<PackExpressionAccessResolution> {
|
||||
if (!packResolver) return 'not-pack';
|
||||
return await packResolver.resolve(packId);
|
||||
}
|
||||
|
||||
function applyReplacements(content: string, replacements: Array<Replacement>): string {
|
||||
if (replacements.length === 0) return content;
|
||||
const sorted = [...replacements].sort((a, b) => b.start - a.start);
|
||||
|
||||
@@ -77,7 +77,6 @@ import {
|
||||
getEmailService,
|
||||
getEmbedService,
|
||||
getEntityAssetService,
|
||||
getExpressionAssetPurger,
|
||||
getFavoriteMemeRepository,
|
||||
getGuildAuditLogService,
|
||||
getGuildRepository,
|
||||
@@ -89,7 +88,6 @@ import {
|
||||
getLimitConfigService,
|
||||
getNcmecSubmissionService,
|
||||
getOAuth2TokenRepository,
|
||||
getPackRepository,
|
||||
getPremiumStateReconciliationQueueService,
|
||||
getPurgeQueue,
|
||||
getRateLimitService,
|
||||
@@ -215,7 +213,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS
|
||||
await ensureVirusScanInitialized();
|
||||
const virusScanService = getVirusScanServiceInstance();
|
||||
const rateLimitService = getRateLimitService();
|
||||
const packRepository = getPackRepository();
|
||||
const emailService = getEmailService();
|
||||
const workerService = getWorkerService();
|
||||
const guildAuditLogService = getGuildAuditLogService();
|
||||
@@ -261,7 +258,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS
|
||||
const apiContext = createApiContext();
|
||||
const {channelService, guildService, inviteService} = createGuildStackServices({
|
||||
apiContext,
|
||||
packRepository,
|
||||
channelRepository,
|
||||
userRepository,
|
||||
guildRepository,
|
||||
@@ -271,7 +267,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS
|
||||
avatarService,
|
||||
entityAssetService,
|
||||
assetDeletionQueue,
|
||||
expressionAssetPurger: getExpressionAssetPurger(),
|
||||
userCacheService,
|
||||
limitConfigService,
|
||||
embedService,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -132,7 +132,6 @@ export const APIErrorCodes = {
|
||||
INVALID_FORM_BODY: 'INVALID_FORM_BODY',
|
||||
INVALID_GRANT: 'INVALID_GRANT',
|
||||
INVALID_HANDOFF_CODE: 'INVALID_HANDOFF_CODE',
|
||||
INVALID_PACK_TYPE: 'INVALID_PACK_TYPE',
|
||||
INVALID_PERMISSIONS_INTEGER: 'INVALID_PERMISSIONS_INTEGER',
|
||||
INVALID_PERMISSIONS_NEGATIVE: 'INVALID_PERMISSIONS_NEGATIVE',
|
||||
INVALID_PHONE_NUMBER: 'INVALID_PHONE_NUMBER',
|
||||
@@ -175,8 +174,6 @@ export const APIErrorCodes = {
|
||||
MAX_GUILDS: 'MAX_GUILDS',
|
||||
NEW_ACCOUNT_GUILD_JOIN_RATE_LIMITED: 'NEW_ACCOUNT_GUILD_JOIN_RATE_LIMITED',
|
||||
MAX_INVITES: 'MAX_INVITES',
|
||||
MAX_PACK_EXPRESSIONS: 'MAX_PACK_EXPRESSIONS',
|
||||
MAX_PACKS: 'MAX_PACKS',
|
||||
MAX_PINS_PER_CHANNEL: 'MAX_PINS_PER_CHANNEL',
|
||||
MESSAGE_TOTAL_ATTACHMENT_SIZE_TOO_LARGE: 'MESSAGE_TOTAL_ATTACHMENT_SIZE_TOO_LARGE',
|
||||
MAX_REACTIONS: 'MAX_REACTIONS',
|
||||
@@ -211,7 +208,6 @@ export const APIErrorCodes = {
|
||||
NOT_OWNER_OF_ADMIN_API_KEY: 'NOT_OWNER_OF_ADMIN_API_KEY',
|
||||
NSFW_CONTENT_AGE_RESTRICTED: 'NSFW_CONTENT_AGE_RESTRICTED',
|
||||
NSFW_EMOJI_STICKER_BLOCKED: 'NSFW_EMOJI_STICKER_BLOCKED',
|
||||
PACK_ACCESS_DENIED: 'PACK_ACCESS_DENIED',
|
||||
PASSKEY_AUTHENTICATION_FAILED: 'PASSKEY_AUTHENTICATION_FAILED',
|
||||
PASSKEYS_DISABLED: 'PASSKEYS_DISABLED',
|
||||
PHONE_ADD_NOT_ELIGIBLE: 'PHONE_ADD_NOT_ELIGIBLE',
|
||||
@@ -290,7 +286,6 @@ export const APIErrorCodes = {
|
||||
UNKNOWN_INVITE: 'UNKNOWN_INVITE',
|
||||
UNKNOWN_MEMBER: 'UNKNOWN_MEMBER',
|
||||
UNKNOWN_MESSAGE: 'UNKNOWN_MESSAGE',
|
||||
UNKNOWN_PACK: 'UNKNOWN_PACK',
|
||||
UNKNOWN_REPORT: 'UNKNOWN_REPORT',
|
||||
UNKNOWN_ROLE: 'UNKNOWN_ROLE',
|
||||
UNKNOWN_STICKER: 'UNKNOWN_STICKER',
|
||||
|
||||
@@ -33,8 +33,6 @@ export const ChannelOverwriteTypesDescriptions: Record<keyof typeof ChannelOverw
|
||||
export const InviteTypes = {
|
||||
GUILD: 0,
|
||||
GROUP_DM: 1,
|
||||
EMOJI_PACK: 2,
|
||||
STICKER_PACK: 3,
|
||||
} as const;
|
||||
export const MessageTypes = {
|
||||
DEFAULT: 0,
|
||||
|
||||
@@ -16,7 +16,6 @@ export const LIMIT_KEYS = [
|
||||
'max_bio_length',
|
||||
'max_bookmarks',
|
||||
'max_channels_per_category',
|
||||
'max_created_packs',
|
||||
'max_custom_backgrounds',
|
||||
'max_embeds_per_message',
|
||||
'max_favorite_meme_tags',
|
||||
@@ -35,10 +34,8 @@ export const LIMIT_KEYS = [
|
||||
'max_guild_stickers_more',
|
||||
'max_guild_stickers',
|
||||
'max_guilds',
|
||||
'max_installed_packs',
|
||||
'max_attachment_file_size',
|
||||
'max_message_length',
|
||||
'max_pack_expressions',
|
||||
'max_private_channels_per_user',
|
||||
'max_reactions_per_message',
|
||||
'max_relationships',
|
||||
@@ -68,7 +65,6 @@ export const LIMIT_KEY_SCOPES: Record<LimitKey, LimitScope> = {
|
||||
max_bio_length: 'user',
|
||||
max_bookmarks: 'user',
|
||||
max_channels_per_category: 'guild',
|
||||
max_created_packs: 'user',
|
||||
max_custom_backgrounds: 'user',
|
||||
max_embeds_per_message: 'both',
|
||||
max_favorite_meme_tags: 'user',
|
||||
@@ -87,10 +83,8 @@ export const LIMIT_KEY_SCOPES: Record<LimitKey, LimitScope> = {
|
||||
max_guild_stickers_more: 'guild',
|
||||
max_guild_stickers: 'guild',
|
||||
max_guilds: 'user',
|
||||
max_installed_packs: 'user',
|
||||
max_attachment_file_size: 'both',
|
||||
max_message_length: 'both',
|
||||
max_pack_expressions: 'guild',
|
||||
max_private_channels_per_user: 'user',
|
||||
max_reactions_per_message: 'guild',
|
||||
max_relationships: 'user',
|
||||
@@ -332,33 +326,6 @@ export const LIMIT_KEY_METADATA: Record<LimitKey, LimitKeyMetadata> = {
|
||||
isToggle: false,
|
||||
unit: 'bytes',
|
||||
},
|
||||
max_pack_expressions: {
|
||||
key: 'max_pack_expressions',
|
||||
label: 'Max Pack Expressions',
|
||||
description: 'Maximum expressions per pack',
|
||||
category: 'expressions',
|
||||
scope: 'guild',
|
||||
isToggle: false,
|
||||
unit: 'count',
|
||||
},
|
||||
max_created_packs: {
|
||||
key: 'max_created_packs',
|
||||
label: 'Max Created Packs',
|
||||
description: 'Maximum expression packs a user can create',
|
||||
category: 'expressions',
|
||||
scope: 'user',
|
||||
isToggle: false,
|
||||
unit: 'count',
|
||||
},
|
||||
max_installed_packs: {
|
||||
key: 'max_installed_packs',
|
||||
label: 'Max Installed Packs',
|
||||
description: 'Maximum expression packs a user can install',
|
||||
category: 'expressions',
|
||||
scope: 'user',
|
||||
isToggle: false,
|
||||
unit: 'count',
|
||||
},
|
||||
max_attachment_file_size: {
|
||||
key: 'max_attachment_file_size',
|
||||
label: 'Max Attachment File Size',
|
||||
|
||||
@@ -62,11 +62,6 @@ export const MAX_FAVORITE_MEMES_PREMIUM = 500;
|
||||
export const MAX_FAVORITE_MEMES_NON_PREMIUM = 50;
|
||||
export const MAX_FAVORITE_MEME_TAGS = 10;
|
||||
export const MAX_FAVORITE_GIFS = 10000;
|
||||
export const MAX_PACK_EXPRESSIONS = 200;
|
||||
export const MAX_CREATED_PACKS_NON_PREMIUM = 0;
|
||||
export const MAX_CREATED_PACKS_PREMIUM = 50;
|
||||
export const MAX_INSTALLED_PACKS_NON_PREMIUM = 0;
|
||||
export const MAX_INSTALLED_PACKS_PREMIUM = 50;
|
||||
export const MAX_VOICE_MESSAGE_DURATION = 1200;
|
||||
export const MAX_MEDIA_DURATION_SECONDS = 86400;
|
||||
export const EMOJI_MAX_SIZE = ASSET_FORMAT_POLICY.emoji.maxBytes;
|
||||
|
||||
@@ -137,14 +137,6 @@ export const LIMIT_TIER_PERKS: ReadonlyArray<LimitTierPerk> = [
|
||||
limitKey: 'max_attachment_file_size',
|
||||
unit: 'bytes',
|
||||
},
|
||||
{
|
||||
id: 'emoji_sticker_packs',
|
||||
type: 'boolean',
|
||||
status: 'coming_soon',
|
||||
i18nKey: 'emoji_sticker_packs',
|
||||
restrictedValue: false,
|
||||
stockValue: true,
|
||||
},
|
||||
{
|
||||
id: 'max_favorite_memes',
|
||||
type: 'numeric',
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ForbiddenError} from '@fluxer/errors/src/domains/core/ForbiddenError';
|
||||
|
||||
export class FeatureAccessError extends ForbiddenError {
|
||||
constructor() {
|
||||
super({code: APIErrorCodes.MISSING_ACCESS});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
|
||||
|
||||
export class InvalidPackTypeError extends BadRequestError {
|
||||
constructor(expectedType: 'emoji' | 'sticker') {
|
||||
super({
|
||||
code: APIErrorCodes.INVALID_PACK_TYPE,
|
||||
messageVariables: {expectedType},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
|
||||
|
||||
export class MaxPackExpressionsError extends BadRequestError {
|
||||
constructor(maxExpressions: number) {
|
||||
super({
|
||||
code: APIErrorCodes.MAX_PACK_EXPRESSIONS,
|
||||
messageVariables: {count: maxExpressions},
|
||||
data: {
|
||||
max_expressions: maxExpressions,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
|
||||
|
||||
export class MaxPackLimitError extends BadRequestError {
|
||||
constructor(packType: 'emoji' | 'sticker', limit: number, action: 'create' | 'install') {
|
||||
super({
|
||||
code: APIErrorCodes.MAX_PACKS,
|
||||
messageVariables: {packType, count: limit, action},
|
||||
data: {
|
||||
pack_type: packType,
|
||||
limit,
|
||||
action,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ForbiddenError} from '@fluxer/errors/src/domains/core/ForbiddenError';
|
||||
|
||||
export class PackAccessDeniedError extends ForbiddenError {
|
||||
constructor() {
|
||||
super({code: APIErrorCodes.PACK_ACCESS_DENIED});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {NotFoundError} from '@fluxer/errors/src/domains/core/NotFoundError';
|
||||
|
||||
export class UnknownPackError extends NotFoundError {
|
||||
constructor() {
|
||||
super({code: APIErrorCodes.UNKNOWN_PACK});
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,6 @@ export const ErrorCodeToI18nKey = {
|
||||
[ValidationErrorCodes.INVALID_OR_EXPIRED_AUTHORIZATION_TICKET]:
|
||||
'auth_and_oauth.invalid_or_expired_authorization_ticket',
|
||||
[ValidationErrorCodes.INVALID_OR_EXPIRED_SSO_STATE]: 'auth_and_oauth.invalid_or_expired_sso_state',
|
||||
[APIErrorCodes.INVALID_PACK_TYPE]: 'invites_and_packs.invalid_pack_type',
|
||||
[APIErrorCodes.INVALID_PERMISSIONS_INTEGER]: 'misc.invalid_permissions_integer',
|
||||
[APIErrorCodes.INVALID_PERMISSIONS_NEGATIVE]: 'misc.invalid_permissions_negative',
|
||||
[APIErrorCodes.INVALID_PHONE_NUMBER]: 'phone.invalid_number',
|
||||
@@ -224,8 +223,6 @@ export const ErrorCodeToI18nKey = {
|
||||
[APIErrorCodes.MAX_GUILDS]: 'channels_and_guilds.max_guilds_reached',
|
||||
[APIErrorCodes.NEW_ACCOUNT_GUILD_JOIN_RATE_LIMITED]: 'channels_and_guilds.new_account_guild_join_rate_limited',
|
||||
[APIErrorCodes.MAX_INVITES]: 'misc_limits.max_invites_reached',
|
||||
[APIErrorCodes.MAX_PACK_EXPRESSIONS]: 'invites_and_packs.max_pack_expressions_reached',
|
||||
[APIErrorCodes.MAX_PACKS]: 'invites_and_packs.max_packs_reached',
|
||||
[APIErrorCodes.MAX_PINS_PER_CHANNEL]: 'misc_limits.max_pins_per_channel_reached',
|
||||
[APIErrorCodes.MAX_REACTIONS]: 'stickers_and_emojis.max_reactions_reached',
|
||||
[APIErrorCodes.MAX_STICKERS]: 'stickers_and_emojis.max_stickers_reached',
|
||||
@@ -261,7 +258,6 @@ export const ErrorCodeToI18nKey = {
|
||||
[APIErrorCodes.NOT_OWNER_OF_ADMIN_API_KEY]: 'permissions.not_owner_of_admin_api_key',
|
||||
[APIErrorCodes.NSFW_CONTENT_AGE_RESTRICTED]: 'content_and_safety.nsfw_age_restricted',
|
||||
[APIErrorCodes.NSFW_EMOJI_STICKER_BLOCKED]: 'content_and_safety.nsfw_emoji_sticker_blocked',
|
||||
[APIErrorCodes.PACK_ACCESS_DENIED]: 'invites_and_packs.pack_access_denied',
|
||||
[APIErrorCodes.PASSKEY_AUTHENTICATION_FAILED]: 'mfa_and_passkeys.passkey_authentication_failed',
|
||||
[APIErrorCodes.PASSKEYS_DISABLED]: 'mfa_and_passkeys.passkeys_disabled',
|
||||
[APIErrorCodes.PHONE_ADD_NOT_ELIGIBLE]: 'phone.add_not_eligible',
|
||||
@@ -341,7 +337,6 @@ export const ErrorCodeToI18nKey = {
|
||||
[APIErrorCodes.UNKNOWN_INVITE]: 'unknown_entities.unknown_invite',
|
||||
[APIErrorCodes.UNKNOWN_MEMBER]: 'unknown_entities.unknown_member',
|
||||
[APIErrorCodes.UNKNOWN_MESSAGE]: 'unknown_entities.unknown_message',
|
||||
[APIErrorCodes.UNKNOWN_PACK]: 'unknown_entities.unknown_pack',
|
||||
[APIErrorCodes.UNKNOWN_REPORT]: 'moderation_and_reports.unknown_report',
|
||||
[APIErrorCodes.UNKNOWN_ROLE]: 'unknown_entities.unknown_role',
|
||||
[APIErrorCodes.UNKNOWN_STICKER]: 'stickers_and_emojis.unknown_sticker',
|
||||
|
||||
@@ -386,15 +386,10 @@ export const ERROR_I18N_MESSAGES = {
|
||||
'invites_and_gifts.unknown_gift_code': 'Unknown gift code.',
|
||||
'invites_and_packs.emoji_requires_access':
|
||||
"You can't use this emoji without access to its community or installed pack.",
|
||||
'invites_and_packs.invalid_pack_type': 'Invalid pack type.',
|
||||
'invites_and_packs.max_animated_emojis_reached':
|
||||
"You've reached the maximum of {count, plural, one {# animated emoji} other {# animated emojis}}.",
|
||||
'invites_and_packs.max_emojis_reached':
|
||||
"You've reached the maximum of {count, plural, one {# emoji} other {# emojis}}.",
|
||||
'invites_and_packs.max_pack_expressions_reached':
|
||||
"You've reached the maximum of {count, plural, one {# pack expression} other {# pack expressions}}.",
|
||||
'invites_and_packs.max_packs_reached': "You've reached the maximum of {count, plural, one {# pack} other {# packs}}.",
|
||||
'invites_and_packs.pack_access_denied': "You don't have permission to access this pack.",
|
||||
'limits.at_least_one_entry_required': 'At least one entry is required.',
|
||||
'limits.base64_length_invalid': 'Base64 string length must be between {min} and {maxLength} characters.',
|
||||
'limits.bucket_required': '`bucket` is required.',
|
||||
@@ -592,7 +587,6 @@ export const ERROR_I18N_MESSAGES = {
|
||||
'unknown_entities.unknown_invite': "Invite wasn't found or is no longer valid.",
|
||||
'unknown_entities.unknown_member': "Member wasn't found in this community.",
|
||||
'unknown_entities.unknown_message': "Message wasn't found.",
|
||||
'unknown_entities.unknown_pack': "Pack wasn't found.",
|
||||
'unknown_entities.unknown_role': "Role wasn't found.",
|
||||
'unknown_entities.unknown_user': "User wasn't found.",
|
||||
'unknown_entities.unknown_user_flag': "The specified user flag isn't recognized.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_AR_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "جارٍ استرداد رمز الهدية. حاول مجددًا بعد قليل.",
|
||||
"invites_and_gifts.unknown_gift_code": "رمز الهدية غير معروف.",
|
||||
"invites_and_packs.emoji_requires_access": "لا يمكنك استخدام هذا الإيموجي بدون الوصول إلى مجتمعه أو الحزمة المثبتة.",
|
||||
"invites_and_packs.invalid_pack_type": "نوع الحزمة غير صحيح.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "لقد وصلت إلى الحد الأقصى وهو {count} {count, plural, one {إيموجي متحرك} two {إيموجيان متحركان} few {إيموجيات متحركة} many {إيموجي متحرك} other {إيموجي متحرك}}.",
|
||||
"invites_and_packs.max_emojis_reached": "لقد وصلت إلى الحد الأقصى وهو {count} {count, plural, one {إيموجي} two {إيموجيان} few {إيموجيات} many {إيموجي} other {إيموجي}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "لقد وصلت إلى الحد الأقصى وهو {count} {count, plural, one {حزمة تعبير} two {حزمتان تعبير} few {حزم تعبير} many {حزمة تعبير} other {حزمة تعبير}}.",
|
||||
"invites_and_packs.max_packs_reached": "لقد وصلت إلى الحد الأقصى وهو {count} {count, plural, one {حزمة} two {حزمتان} few {حزم} many {حزمة} other {حزمة}}.",
|
||||
"invites_and_packs.pack_access_denied": "ليس لديك إذن الوصول إلى هذه الحزمة.",
|
||||
"limits.at_least_one_entry_required": "مطلوب إدخال واحد على الأقل.",
|
||||
"limits.base64_length_invalid": "يجب أن يتراوح طول سلسلة Base64 بين {min} و {maxLength} حرف.",
|
||||
"limits.bucket_required": "`bucket` مطلوب.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_AR_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "لم يتم العثور على الدعوة أو لم تعد صالحة.",
|
||||
"unknown_entities.unknown_member": "لم يتم العثور على العضو في هذا المجتمع.",
|
||||
"unknown_entities.unknown_message": "لم يتم العثور على الرسالة.",
|
||||
"unknown_entities.unknown_pack": "لم يتم العثور على الحزمة.",
|
||||
"unknown_entities.unknown_role": "لم يتم العثور على الدور.",
|
||||
"unknown_entities.unknown_user": "لم يتم العثور على المستخدم.",
|
||||
"unknown_entities.unknown_user_flag": "علامة المستخدم المحددة غير معروفة.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_BG_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Активирането на кода за подарък е в процес. Опитай отново след малко.",
|
||||
"invites_and_gifts.unknown_gift_code": "Неизвестен код за подарък.",
|
||||
"invites_and_packs.emoji_requires_access": "Не можеш да използваш това емоджи без достъп до неговата общност или инсталиран пакет.",
|
||||
"invites_and_packs.invalid_pack_type": "Невалиден тип пакет.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Достигнал си максимума от {count} анимирани {count, plural, one {емоджи} other {емоджита}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Достигнал си максимума от {count} {count, plural, one {емоджи} other {емоджита}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Достигнал си максимума от {count} {count, plural, one {израз в пакет} other {израза в пакет}}.",
|
||||
"invites_and_packs.max_packs_reached": "Достигнал си максимума от {count} {count, plural, one {пакет} other {пакета}}.",
|
||||
"invites_and_packs.pack_access_denied": "Нямаш разрешение за достъп до този пакет.",
|
||||
"limits.at_least_one_entry_required": "Необходим е поне един запис.",
|
||||
"limits.base64_length_invalid": "Дължината на Base64 низа трябва да е между {min} и {maxLength} символа.",
|
||||
"limits.bucket_required": "Изисква се `bucket`.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_BG_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Поканата не е намерена или вече не е валидна.",
|
||||
"unknown_entities.unknown_member": "Членът не е намерен в тази общност.",
|
||||
"unknown_entities.unknown_message": "Съобщението не е намерено.",
|
||||
"unknown_entities.unknown_pack": "Пакетът не е намерен.",
|
||||
"unknown_entities.unknown_role": "Ролята не е намерена.",
|
||||
"unknown_entities.unknown_user": "Потребителят не е намерен.",
|
||||
"unknown_entities.unknown_user_flag": "Посоченият потребителски флаг не е разпознат.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_CS_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Uplatnění dárkového kódu probíhá. Zkus to prosím za chvíli znovu.",
|
||||
"invites_and_gifts.unknown_gift_code": "Neznámý dárkový kód.",
|
||||
"invites_and_packs.emoji_requires_access": "Toto emoji nemůžeš použít bez přístupu k jeho komunitě nebo k nainstalovanému balíčku.",
|
||||
"invites_and_packs.invalid_pack_type": "Neplatný typ balíčku.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Dosáhl jsi maximálního počtu {count} animovaných {count, plural, one {emoji} few {emoji} many {emoji} other {emoji}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Dosáhl jsi maximálního počtu {count} {count, plural, one {emoji} few {emoji} many {emoji} other {emoji}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Dosáhl jsi maximálního počtu {count} {count, plural, one {výraz balíčku} few {výrazy balíčku} many {výrazy balíčku} other {výrazů balíčku}}.",
|
||||
"invites_and_packs.max_packs_reached": "Dosáhl jsi maximálního počtu {count} {count, plural, one {balíček} few {balíčky} many {balíčky} other {balíčků}}.",
|
||||
"invites_and_packs.pack_access_denied": "Nemáš oprávnění k přístupu k tomuto balíčku.",
|
||||
"limits.at_least_one_entry_required": "Je vyžadován alespoň jeden záznam.",
|
||||
"limits.base64_length_invalid": "Délka řetězce Base64 musí být mezi {min} a {maxLength} znaky.",
|
||||
"limits.bucket_required": "`bucket` je povinný.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_CS_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Pozvánka nebyla nalezena nebo už není platná.",
|
||||
"unknown_entities.unknown_member": "Člen nebyl v této komunitě nalezen.",
|
||||
"unknown_entities.unknown_message": "Zpráva nebyla nalezena.",
|
||||
"unknown_entities.unknown_pack": "Balíček nebyl nalezen.",
|
||||
"unknown_entities.unknown_role": "Role nebyla nalezena.",
|
||||
"unknown_entities.unknown_user": "Uživatel nebyl nalezen.",
|
||||
"unknown_entities.unknown_user_flag": "Zadaný uživatelský příznak není rozpoznán.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_DA_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Indløsning af gavekode er i gang. Prøv igen om et øjeblik.",
|
||||
"invites_and_gifts.unknown_gift_code": "Ukendt gavekode.",
|
||||
"invites_and_packs.emoji_requires_access": "Du kan ikke bruge denne emoji uden adgang til dens community eller installerede pakke.",
|
||||
"invites_and_packs.invalid_pack_type": "Ugyldig pakketype.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Du har nået maksimum på {count} {count, plural, one {animeret emoji} other {animerede emojis}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Du har nået maksimum på {count} {count, plural, one {emoji} other {emojis}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Du har nået maksimum på {count} {count, plural, one {pakkeudtryk} other {pakkeudtryk}}.",
|
||||
"invites_and_packs.max_packs_reached": "Du har nået maksimum på {count} {count, plural, one {pakke} other {pakker}}.",
|
||||
"invites_and_packs.pack_access_denied": "Du har ikke tilladelse til at få adgang til denne pakke.",
|
||||
"limits.at_least_one_entry_required": "Mindst ét element er nødvendigt.",
|
||||
"limits.base64_length_invalid": "Base64-strengens længde skal være mellem {min} og {maxLength} tegn.",
|
||||
"limits.bucket_required": "`bucket` er påkrævet.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_DA_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Invitation blev ikke fundet eller er ikke længere gyldig.",
|
||||
"unknown_entities.unknown_member": "Medlem blev ikke fundet i denne community.",
|
||||
"unknown_entities.unknown_message": "Besked blev ikke fundet.",
|
||||
"unknown_entities.unknown_pack": "Pakke blev ikke fundet.",
|
||||
"unknown_entities.unknown_role": "Rolle blev ikke fundet.",
|
||||
"unknown_entities.unknown_user": "Bruger blev ikke fundet.",
|
||||
"unknown_entities.unknown_user_flag": "Det angivne brugerflag genkendes ikke.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_DE_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Die Verwendung des Geschenkcodes wird bearbeitet. Versuch es gleich noch einmal.",
|
||||
"invites_and_gifts.unknown_gift_code": "Unbekannter Geschenkcode.",
|
||||
"invites_and_packs.emoji_requires_access": "Du kannst dieses Emoji nicht ohne Zugriff auf seine Community oder ein installiertes Paket verwenden.",
|
||||
"invites_and_packs.invalid_pack_type": "Ungültiger Pack-Typ.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Du hast die maximale Anzahl von {count} animierten {count, plural, one {Emoji} other {Emojis}} erreicht.",
|
||||
"invites_and_packs.max_emojis_reached": "Du hast die maximale Anzahl von {count} {count, plural, one {Emoji} other {Emojis}} erreicht.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Du hast die maximale Anzahl von {count} Pack-{count, plural, one {Expression} other {Expressions}} erreicht.",
|
||||
"invites_and_packs.max_packs_reached": "Du hast die maximale Anzahl von {count} {count, plural, one {Pack} other {Packs}} erreicht.",
|
||||
"invites_and_packs.pack_access_denied": "Du hast keine Berechtigung, auf dieses Pack zuzugreifen.",
|
||||
"limits.at_least_one_entry_required": "Mindestens ein Eintrag ist erforderlich.",
|
||||
"limits.base64_length_invalid": "Die Länge der Base64-Zeichenkette muss zwischen {min} und {maxLength} Zeichen liegen.",
|
||||
"limits.bucket_required": "`Bucket` ist erforderlich.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_DE_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Einladung nicht gefunden oder nicht mehr gültig.",
|
||||
"unknown_entities.unknown_member": "Mitglied in dieser Community nicht gefunden.",
|
||||
"unknown_entities.unknown_message": "Nachricht nicht gefunden.",
|
||||
"unknown_entities.unknown_pack": "Paket nicht gefunden.",
|
||||
"unknown_entities.unknown_role": "Rolle nicht gefunden.",
|
||||
"unknown_entities.unknown_user": "Benutzer nicht gefunden.",
|
||||
"unknown_entities.unknown_user_flag": "Das angegebene Benutzer-Flag wird nicht erkannt.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_EL_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Η εξαργύρωση του κωδικού δώρου βρίσκεται σε εξέλιξη. Δοκίμασε ξανά σε λίγο.",
|
||||
"invites_and_gifts.unknown_gift_code": "Άγνωστος κωδικός δώρου.",
|
||||
"invites_and_packs.emoji_requires_access": "Δεν μπορείς να χρησιμοποιήσεις αυτό το emoji χωρίς πρόσβαση στην κοινότητά του ή σε πακέτο που έχεις εγκαταστήσει.",
|
||||
"invites_and_packs.invalid_pack_type": "Μη έγκυρος τύπος πακέτου.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Έχεις φτάσει το μέγιστο των {count} {count, plural, one {κινούμενου emoji} other {κινούμενων emoji}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Έχεις φτάσει το μέγιστο των {count} {count, plural, one {emoji} other {emoji}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Έχεις φτάσει το μέγιστο των {count} {count, plural, one {έκφρασης πακέτου} other {εκφράσεων πακέτου}}.",
|
||||
"invites_and_packs.max_packs_reached": "Έχεις φτάσει το μέγιστο των {count} {count, plural, one {πακέτου} other {πακέτων}}.",
|
||||
"invites_and_packs.pack_access_denied": "Δεν έχεις δικαίωμα πρόσβασης σε αυτό το πακέτο.",
|
||||
"limits.at_least_one_entry_required": "Απαιτείται τουλάχιστον μία εγγραφή.",
|
||||
"limits.base64_length_invalid": "Το μήκος της συμβολοσειράς Base64 πρέπει να είναι μεταξύ {min} και {maxLength} χαρακτήρων.",
|
||||
"limits.bucket_required": "Το `bucket` απαιτείται.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_EL_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Δεν βρήκαμε την πρόσκληση ή δεν ισχύει πλέον.",
|
||||
"unknown_entities.unknown_member": "Δεν βρήκαμε το μέλος σε αυτήν την κοινότητα.",
|
||||
"unknown_entities.unknown_message": "Δεν βρήκαμε το μήνυμα.",
|
||||
"unknown_entities.unknown_pack": "Δεν βρήκαμε το πακέτο.",
|
||||
"unknown_entities.unknown_role": "Δεν βρήκαμε τον ρόλο.",
|
||||
"unknown_entities.unknown_user": "Δεν βρήκαμε τον χρήστη.",
|
||||
"unknown_entities.unknown_user_flag": "Η σημαία χρήστη που καθορίστηκε δεν αναγνωρίζεται.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_EN_GB_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Gift code redemption is in progress. Try again in a moment.",
|
||||
"invites_and_gifts.unknown_gift_code": "Unknown gift code.",
|
||||
"invites_and_packs.emoji_requires_access": "You can't use this emoji without access to its community or installed pack.",
|
||||
"invites_and_packs.invalid_pack_type": "Invalid pack type.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "You've reached the maximum of {count} animated {count, plural, one {emoji} other {emojis}}.",
|
||||
"invites_and_packs.max_emojis_reached": "You've reached the maximum of {count} {count, plural, one {emoji} other {emojis}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "You've reached the maximum of {count} pack {count, plural, one {expression} other {expressions}}.",
|
||||
"invites_and_packs.max_packs_reached": "You've reached the maximum of {count} {count, plural, one {pack} other {packs}}.",
|
||||
"invites_and_packs.pack_access_denied": "You don't have permission to access this pack.",
|
||||
"limits.at_least_one_entry_required": "At least one entry is required.",
|
||||
"limits.base64_length_invalid": "Base64 string length must be between {min} and {maxLength} characters.",
|
||||
"limits.bucket_required": "`bucket` is required.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_EN_GB_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Invite wasn't found or is no longer valid.",
|
||||
"unknown_entities.unknown_member": "Member wasn't found in this community.",
|
||||
"unknown_entities.unknown_message": "Message wasn't found.",
|
||||
"unknown_entities.unknown_pack": "Pack wasn't found.",
|
||||
"unknown_entities.unknown_role": "Role wasn't found.",
|
||||
"unknown_entities.unknown_user": "User wasn't found.",
|
||||
"unknown_entities.unknown_user_flag": "The specified user flag isn't recognized.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_ES_419_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "El canje del código de regalo está en proceso. Intenta de nuevo en un momento.",
|
||||
"invites_and_gifts.unknown_gift_code": "Código de regalo desconocido.",
|
||||
"invites_and_packs.emoji_requires_access": "No puedes usar este emoji sin acceso a su comunidad o paquete instalado.",
|
||||
"invites_and_packs.invalid_pack_type": "Tipo de paquete no válido.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Has alcanzado el máximo de {count} {count, plural, one {emoji animado} other {emojis animados}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Has alcanzado el máximo de {count} {count, plural, one {emoji} other {emojis}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Has alcanzado el máximo de {count} {count, plural, one {expresión del paquete} other {expresiones del paquete}}.",
|
||||
"invites_and_packs.max_packs_reached": "Has alcanzado el máximo de {count} {count, plural, one {paquete} other {paquetes}}.",
|
||||
"invites_and_packs.pack_access_denied": "No tienes permiso para acceder a este paquete.",
|
||||
"limits.at_least_one_entry_required": "Se requiere al menos una entrada.",
|
||||
"limits.base64_length_invalid": "La longitud de la cadena Base64 debe estar entre {min} y {maxLength} caracteres.",
|
||||
"limits.bucket_required": "`bucket` es requerido.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_ES_419_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "No se encontró la invitación o ya no es válida.",
|
||||
"unknown_entities.unknown_member": "No se encontró al miembro en esta comunidad.",
|
||||
"unknown_entities.unknown_message": "No se encontró el mensaje.",
|
||||
"unknown_entities.unknown_pack": "No se encontró el paquete.",
|
||||
"unknown_entities.unknown_role": "No se encontró el rol.",
|
||||
"unknown_entities.unknown_user": "No se encontró al usuario.",
|
||||
"unknown_entities.unknown_user_flag": "El indicador de usuario especificado no se reconoce.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_ES_ES_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "El canje del código de regalo está en curso. Inténtalo de nuevo en un momento.",
|
||||
"invites_and_gifts.unknown_gift_code": "Código de regalo desconocido.",
|
||||
"invites_and_packs.emoji_requires_access": "No puedes usar este emoji sin acceso a su comunidad o paquete instalado.",
|
||||
"invites_and_packs.invalid_pack_type": "Tipo de paquete no válido.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Has alcanzado el máximo de {count} {count, plural, one {emoji animado} other {emojis animados}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Has alcanzado el máximo de {count} {count, plural, one {emoji} other {emojis}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Has alcanzado el máximo de {count} {count, plural, one {expresión de pack} other {expresiones de pack}}.",
|
||||
"invites_and_packs.max_packs_reached": "Has alcanzado el máximo de {count} {count, plural, one {paquete} other {paquetes}}.",
|
||||
"invites_and_packs.pack_access_denied": "No tienes permiso para acceder a este pack.",
|
||||
"limits.at_least_one_entry_required": "Es necesaria al menos una entrada.",
|
||||
"limits.base64_length_invalid": "La longitud de la cadena Base64 debe estar entre {min} y {maxLength} caracteres.",
|
||||
"limits.bucket_required": "`bucket` es necesario.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_ES_ES_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "No se encontró la invitación o ya no es válida.",
|
||||
"unknown_entities.unknown_member": "No se encontró al miembro en esta comunidad.",
|
||||
"unknown_entities.unknown_message": "No se encontró el mensaje.",
|
||||
"unknown_entities.unknown_pack": "No se encontró el paquete.",
|
||||
"unknown_entities.unknown_role": "No se encontró el rol.",
|
||||
"unknown_entities.unknown_user": "No se encontró el usuario.",
|
||||
"unknown_entities.unknown_user_flag": "El indicador de usuario especificado no se reconoce.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_FI_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Lahjakoodin lunastus on käynnissä. Yritä uudelleen hetken kuluttua.",
|
||||
"invites_and_gifts.unknown_gift_code": "Tuntematon lahjakoodi.",
|
||||
"invites_and_packs.emoji_requires_access": "Et voi käyttää tätä emojia ilman pääsyä sen yhteisöön tai asennettua pakettia.",
|
||||
"invites_and_packs.invalid_pack_type": "Virheellinen pakettityyppi.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Olet saavuttanut enimmäismäärän {count} {count, plural, one {animoitu emoji} other {animoitua emojia}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Olet saavuttanut enimmäismäärän {count} {count, plural, one {emoji} other {emojia}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Olet saavuttanut enimmäismäärän {count} {count, plural, one {pakettilauseke} other {pakettilauseketta}}.",
|
||||
"invites_and_packs.max_packs_reached": "Olet saavuttanut enimmäismäärän {count} {count, plural, one {paketti} other {pakettia}}.",
|
||||
"invites_and_packs.pack_access_denied": "Sinulla ei ole käyttöoikeutta tähän pakettiin.",
|
||||
"limits.at_least_one_entry_required": "Vähintään yksi merkintä vaaditaan.",
|
||||
"limits.base64_length_invalid": "Base64-merkkijonon pituuden on oltava {min}–{maxLength} merkkiä.",
|
||||
"limits.bucket_required": "`bucket` on pakollinen.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_FI_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Kutsua ei löytynyt tai se ei ole enää voimassa.",
|
||||
"unknown_entities.unknown_member": "Jäsentä ei löytynyt tästä yhteisöstä.",
|
||||
"unknown_entities.unknown_message": "Viestiä ei löytynyt.",
|
||||
"unknown_entities.unknown_pack": "Pakettia ei löytynyt.",
|
||||
"unknown_entities.unknown_role": "Roolia ei löytynyt.",
|
||||
"unknown_entities.unknown_user": "Käyttäjää ei löytynyt.",
|
||||
"unknown_entities.unknown_user_flag": "Määritettyä käyttäjälippua ei tunnisteta.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_FR_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "L'utilisation du code cadeau est en cours. Réessaye dans un instant.",
|
||||
"invites_and_gifts.unknown_gift_code": "Code cadeau inconnu.",
|
||||
"invites_and_packs.emoji_requires_access": "Tu ne peux pas utiliser cet emoji sans accès à sa communauté ou à son pack d'emojis.",
|
||||
"invites_and_packs.invalid_pack_type": "Type de pack non valide.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Tu as atteint le maximum de {count} {count, plural, one {emoji animé} other {emojis animés}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Tu as atteint le maximum de {count} {count, plural, one {emoji} other {emojis}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Tu as atteint le maximum de {count} {count, plural, one {expression du pack} other {expressions du pack}}.",
|
||||
"invites_and_packs.max_packs_reached": "Tu as atteint le maximum de {count} {count, plural, one {pack} other {packs}}.",
|
||||
"invites_and_packs.pack_access_denied": "Tu n'as pas la permission d'accéder à ce pack.",
|
||||
"limits.at_least_one_entry_required": "Au moins un élément est requis.",
|
||||
"limits.base64_length_invalid": "La longueur de la chaîne Base64 doit être comprise entre {min} et {maxLength} caractères.",
|
||||
"limits.bucket_required": "`bucket` est obligatoire.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_FR_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Invitation introuvable ou n'est plus valide.",
|
||||
"unknown_entities.unknown_member": "Membre introuvable dans cette communauté.",
|
||||
"unknown_entities.unknown_message": "Message introuvable.",
|
||||
"unknown_entities.unknown_pack": "Pack introuvable.",
|
||||
"unknown_entities.unknown_role": "Rôle introuvable.",
|
||||
"unknown_entities.unknown_user": "Utilisateur introuvable.",
|
||||
"unknown_entities.unknown_user_flag": "L'option utilisateur spécifiée n'est pas reconnue.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_HE_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "מימוש קוד המתנה בעיצומו. ניתן לנסות שוב בעוד רגע.",
|
||||
"invites_and_gifts.unknown_gift_code": "קוד מתנה לא ידוע.",
|
||||
"invites_and_packs.emoji_requires_access": "לא ניתן להשתמש באימוג'י זה ללא גישה לקהילה המקורית שלו או לחבילת אימוג'י מותקנת.",
|
||||
"invites_and_packs.invalid_pack_type": "סוג חבילה שגוי.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "הגעת למקסימום של {count} {count, plural, one {אימוג'י מונפש} other {אימוג'ים מונפשים}}.",
|
||||
"invites_and_packs.max_emojis_reached": "הגעת למקסימום של {count} {count, plural, one {אימוג'י} other {אימוג'ים}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "הגעת למגבלה של {count} {count, plural, one {ביטוי חבילה} other {ביטויי חבילה}}.",
|
||||
"invites_and_packs.max_packs_reached": "הגעת למגבלה של {count} {count, plural, one {חבילה} other {חבילות}}.",
|
||||
"invites_and_packs.pack_access_denied": "אין לך הרשאה לגשת לחבילה זו.",
|
||||
"limits.at_least_one_entry_required": "יש להזין לפחות רשומה אחת.",
|
||||
"limits.base64_length_invalid": "אורך מחרוזת Base64 צריך להיות בין {min} ל-{maxLength} תווים.",
|
||||
"limits.bucket_required": "`bucket` נדרש.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_HE_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "ההזמנה לא נמצאה או שאינה תקפה יותר.",
|
||||
"unknown_entities.unknown_member": "החבר לא נמצא בקהילה זו.",
|
||||
"unknown_entities.unknown_message": "ההודעה לא נמצאה.",
|
||||
"unknown_entities.unknown_pack": "החבילה לא נמצאה.",
|
||||
"unknown_entities.unknown_role": "התפקיד לא נמצא.",
|
||||
"unknown_entities.unknown_user": "המשתמש לא נמצא.",
|
||||
"unknown_entities.unknown_user_flag": "דגל המשתמש שצוין אינו מזוהה.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_HI_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "गिफ्ट कोड रिडीम किया जा रहा है। थोड़ी देर में फिर से कोशिश करें।",
|
||||
"invites_and_gifts.unknown_gift_code": "अज्ञात गिफ्ट कोड।",
|
||||
"invites_and_packs.emoji_requires_access": "आप इस इमोजी का इस्तेमाल इसकी कम्युनिटी या इंस्टॉल किए गए पैक तक पहुँच के बिना नहीं कर सकते।",
|
||||
"invites_and_packs.invalid_pack_type": "अमान्य पैक टाइप।",
|
||||
"invites_and_packs.max_animated_emojis_reached": "आप अधिकतम {count} एनिमेटेड {count, plural, one {इमोजी} other {इमोजी}} तक पहुँच गए हैं।",
|
||||
"invites_and_packs.max_emojis_reached": "आप अधिकतम {count} {count, plural, one {इमोजी} other {इमोजी}} तक पहुँच गए हैं।",
|
||||
"invites_and_packs.max_pack_expressions_reached": "आप अधिकतम {count} पैक {count, plural, one {एक्सप्रेशन} other {एक्सप्रेशन}} तक पहुँच गए हैं।",
|
||||
"invites_and_packs.max_packs_reached": "आप अधिकतम {count} {count, plural, one {पैक} other {पैक}} तक पहुँच गए हैं।",
|
||||
"invites_and_packs.pack_access_denied": "आपके पास इस पैक को एक्सेस करने की अनुमति नहीं है।",
|
||||
"limits.at_least_one_entry_required": "कम से कम एक एंट्री ज़रूरी है।",
|
||||
"limits.base64_length_invalid": "Base64 स्ट्रिंग की लंबाई {min} और {maxLength} कैरेक्टर के बीच होनी चाहिए।",
|
||||
"limits.bucket_required": "`bucket` ज़रूरी है।",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_HI_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "इन्वाइट नहीं मिला या अब मान्य नहीं है.",
|
||||
"unknown_entities.unknown_member": "इस कम्युनिटी में मेंबर नहीं मिला.",
|
||||
"unknown_entities.unknown_message": "मैसेज नहीं मिला.",
|
||||
"unknown_entities.unknown_pack": "पैक नहीं मिला.",
|
||||
"unknown_entities.unknown_role": "रोल नहीं मिला.",
|
||||
"unknown_entities.unknown_user": "यूज़र नहीं मिला.",
|
||||
"unknown_entities.unknown_user_flag": "निर्दिष्ट यूज़र फ्लैग पहचाना नहीं गया।",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_HR_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Iskorištavanje poklon-koda je u tijeku. Pokušaj ponovno za trenutak.",
|
||||
"invites_and_gifts.unknown_gift_code": "Nepoznat poklon-kod.",
|
||||
"invites_and_packs.emoji_requires_access": "Ne možeš koristiti ovaj emoji bez pristupa njegovoj zajednici ili instaliranom paketu.",
|
||||
"invites_and_packs.invalid_pack_type": "Neispravan tip paketa.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Dosegao si maksimalan broj od {count} {count, plural, one {animirani emoji} few {animirana emojija} other {animiranih emojija}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Dosegao si maksimalan broj od {count} {count, plural, one {emoji} few {emojija} other {emojija}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Dosegao si maksimalan broj od {count} {count, plural, one {izraz paketa} few {izraza paketa} other {izraza paketa}}.",
|
||||
"invites_and_packs.max_packs_reached": "Dosegao si maksimalan broj od {count} {count, plural, one {paket} few {paketa} other {paketa}}.",
|
||||
"invites_and_packs.pack_access_denied": "Nemaš dopuštenje za pristup ovom paketu.",
|
||||
"limits.at_least_one_entry_required": "Potreban je barem jedan unos.",
|
||||
"limits.base64_length_invalid": "Duljina Base64 niza mora biti između {min} i {maxLength} znakova.",
|
||||
"limits.bucket_required": "`bucket` je obavezan.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_HR_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "Pozivnica nije pronađena ili više nije važeća.",
|
||||
"unknown_entities.unknown_member": "Član nije pronađen u ovoj zajednici.",
|
||||
"unknown_entities.unknown_message": "Poruka nije pronađena.",
|
||||
"unknown_entities.unknown_pack": "Paket nije pronađen.",
|
||||
"unknown_entities.unknown_role": "Uloga nije pronađena.",
|
||||
"unknown_entities.unknown_user": "Korisnik nije pronađen.",
|
||||
"unknown_entities.unknown_user_flag": "Navedena korisnička zastavica nije prepoznata.",
|
||||
|
||||
@@ -341,12 +341,8 @@ const ERROR_I18N_HU_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"invites_and_gifts.stripe_gift_redemption_in_progress": "Az ajándékkód beváltása folyamatban van. Próbáld újra kis idő múlva.",
|
||||
"invites_and_gifts.unknown_gift_code": "Ismeretlen ajándékkód.",
|
||||
"invites_and_packs.emoji_requires_access": "Ezt az emojit nem használhatod, ha nincs hozzáférésed a közösségéhez vagy az elérhető csomaghoz.",
|
||||
"invites_and_packs.invalid_pack_type": "Érvénytelen csomagtípus.",
|
||||
"invites_and_packs.max_animated_emojis_reached": "Elérted a maximális {count} {count, plural, one {animált emoji} other {animált emoji}}.",
|
||||
"invites_and_packs.max_emojis_reached": "Elérted a maximális {count} {count, plural, one {emoji} other {emoji}}.",
|
||||
"invites_and_packs.max_pack_expressions_reached": "Elérted a maximális {count} {count, plural, one {csomagkifejezés} other {csomagkifejezés}}.",
|
||||
"invites_and_packs.max_packs_reached": "Elérted a maximális {count} {count, plural, one {csomag} other {csomag}}.",
|
||||
"invites_and_packs.pack_access_denied": "Nincs engedélyed a csomag eléréséhez.",
|
||||
"limits.at_least_one_entry_required": "Legalább egy bejegyzés szükséges.",
|
||||
"limits.base64_length_invalid": "A Base64 karakterlánc hossza {min} és {maxLength} karakter közötti lehet.",
|
||||
"limits.bucket_required": "`bucket` szükséges.",
|
||||
@@ -507,7 +503,6 @@ const ERROR_I18N_HU_MESSAGES = defineErrorI18nLocaleMessages({
|
||||
"unknown_entities.unknown_invite": "A meghívó nem található, vagy már nem érvényes.",
|
||||
"unknown_entities.unknown_member": "A tag nem található ebben a közösségben.",
|
||||
"unknown_entities.unknown_message": "Az üzenet nem található.",
|
||||
"unknown_entities.unknown_pack": "A csomag nem található.",
|
||||
"unknown_entities.unknown_role": "A szerep nem található.",
|
||||
"unknown_entities.unknown_user": "A felhasználó nem található.",
|
||||
"unknown_entities.unknown_user_flag": "A megadott felhasználói jelölés nem ismert.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user