Translation: Add Cocoon info (#6646)

This commit is contained in:
zubiden 2026-02-22 23:43:03 +01:00 committed by Alexander Zinchuk
parent 6605a90606
commit baff436709
29 changed files with 352 additions and 43 deletions

View File

@ -84,6 +84,12 @@ declare module '*.jpg' {
const url: string; const url: string;
export default url; export default url;
} }
declare module '*.webp' {
const url: string;
export default url;
}
declare module '*.svg' { declare module '*.svg' {
const url: string; const url: string;
export default url; export default url;

BIN
src/assets/cocoon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -2608,6 +2608,23 @@
"SettingsDataClearMediaCache" = "Clear Media Cache"; "SettingsDataClearMediaCache" = "Clear Media Cache";
"SettingsDataClearMediaCacheDescription" = "Deletes locally cached media for this account"; "SettingsDataClearMediaCacheDescription" = "Deletes locally cached media for this account";
"SettingsDataClearMediaDone" = "Media cache cleared"; "SettingsDataClearMediaDone" = "Media cache cleared";
"TranslateMenuCocoon" = "Translations are powered by 🥚 **Cocoon**. {link}";
"TranslateMenuCocoonLinkText" = "How does it work?";
"CocoonTitle" = "Cocoon";
"CocoonDescription" = "Cocoon (**Co**nfidential **Co**mpute **O**pen **N**etwork)\nhandles AI tasks safely and efficiently.";
"CocoonFeature1Title" = "Private";
"CocoonFeature1Text" = "No third party can access any data, such as translations, inside **{username}**.";
"CocoonFeature1Username" = "@cocoon";
"CocoonFeature1UsernameLink" = "https://t.me/cocoon";
"CocoonFeature2Title" = "Efficient";
"CocoonFeature2Text" = "Cocoon has allowed Telegram to reduce translation cost by 6x.";
"CocoonFeature3Title" = "For Everyone";
"CocoonFeature3Text" = "Any developer can use Cocoon for AI features. Learn more at **{link}**.";
"CocoonFeature3LinkText" = "cocoon.org";
"CocoonFeature3Link" = "https://cocoon.org";
"CocoonFooterText" = "Want to integrate Cocoon into your projects?\nReach out **{link}**";
"CocoonFooterLinkText" = "directly to @cocoon";
"CocoonFooterLink" = "https://t.me/cocoon?direct";
"ChatListAuctionTitle_one" = "Active Auction"; "ChatListAuctionTitle_one" = "Active Auction";
"ChatListAuctionTitle_other" = "{count} Active Auctions"; "ChatListAuctionTitle_other" = "{count} Active Auctions";
"ChatListAuctionWinning" = "You are winning"; "ChatListAuctionWinning" = "You are winning";

View File

@ -108,3 +108,4 @@ export { default as ProfileRatingModal } from '../components/modals/profileRatin
export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal'; export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal'; export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal';
export { default as QuickChatPickerModal } from '../components/modals/quickChatPicker/QuickChatPickerModal'; export { default as QuickChatPickerModal } from '../components/modals/quickChatPicker/QuickChatPickerModal';
export { default as CocoonModal } from '../components/modals/cocoon/CocoonModal';

View File

@ -17,6 +17,8 @@ const DEFAULT_PARTICLE_PARAMS = {
centerShift: [0, -36] as const, centerShift: [0, -36] as const,
}; };
const MIN_BURST_INTERVAL = 8;
const InteractiveSparkles = ({ const InteractiveSparkles = ({
color = 'purple', color = 'purple',
centerShift = DEFAULT_PARTICLE_PARAMS.centerShift, centerShift = DEFAULT_PARTICLE_PARAMS.centerShift,
@ -25,6 +27,7 @@ const InteractiveSparkles = ({
onRequestAnimation, onRequestAnimation,
}: OwnProps) => { }: OwnProps) => {
const canvasRef = useRef<HTMLCanvasElement>(); const canvasRef = useRef<HTMLCanvasElement>();
const lastBurstTimeRef = useRef<number>(0);
useLayoutEffect(() => { useLayoutEffect(() => {
if (isDisabled) return undefined; if (isDisabled) return undefined;
@ -41,6 +44,11 @@ const InteractiveSparkles = ({
const animate = () => { const animate = () => {
if (isDisabled) return; if (isDisabled) return;
const now = Date.now();
if (now - lastBurstTimeRef.current < MIN_BURST_INTERVAL) return;
lastBurstTimeRef.current = now;
setupParticles(canvasRef.current!, { setupParticles(canvasRef.current!, {
color: PARTICLE_COLORS[`${color}Gradient`], color: PARTICLE_COLORS[`${color}Gradient`],
centerShift, centerShift,

View File

@ -8,6 +8,7 @@ import type { IAnchorPosition, MessageListType, ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { ManagementScreens } from '../../types'; import { ManagementScreens } from '../../types';
import { COCOON_EMOJI_ID } from '../../config';
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
import { import {
getHasAdminRight, getHasAdminRight,
@ -38,11 +39,14 @@ import { isUserId } from '../../util/entities/ids';
import focusNoScroll from '../../util/focusNoScroll'; import focusNoScroll from '../../util/focusNoScroll';
import { useHotkeys } from '../../hooks/useHotkeys'; import { useHotkeys } from '../../hooks/useHotkeys';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import CustomEmoji from '../common/CustomEmoji';
import Button from '../ui/Button'; import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../ui/DropdownMenu';
import Link from '../ui/Link';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
import MenuSeparator from '../ui/MenuSeparator'; import MenuSeparator from '../ui/MenuSeparator';
import HeaderMenuContainer from './HeaderMenuContainer.async'; import HeaderMenuContainer from './HeaderMenuContainer.async';
@ -142,9 +146,12 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
unblockUser, unblockUser,
setViewForumAsMessages, setViewForumAsMessages,
openFrozenAccountModal, openFrozenAccountModal,
openCocoonModal,
} = getActions(); } = getActions();
const menuButtonRef = useRef<HTMLButtonElement>(); const menuButtonRef = useRef<HTMLButtonElement>();
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [menuAnchor, setMenuAnchor] = useState<IAnchorPosition | undefined>(undefined); const [menuAnchor, setMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
@ -166,7 +173,8 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
joinChannel({ chatId }); joinChannel({ chatId });
if (shouldSendJoinRequest) { if (shouldSendJoinRequest) {
showNotification({ showNotification({
message: isChannel ? lang('RequestToJoinChannelSentDescription') : lang('RequestToJoinGroupSentDescription'), message: isChannel ? oldLang('RequestToJoinChannelSentDescription')
: oldLang('RequestToJoinGroupSentDescription'),
}); });
} }
}); });
@ -239,21 +247,21 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
const getTextWithLanguage = useCallback((langKey: string, langCode: string) => { const getTextWithLanguage = useCallback((langKey: string, langCode: string) => {
const simplified = langCode.split('-')[0]; const simplified = langCode.split('-')[0];
const translationKey = `TranslateLanguage${simplified.toUpperCase()}`; const translationKey = `TranslateLanguage${simplified.toUpperCase()}`;
const name = lang(translationKey); const name = oldLang(translationKey);
if (name !== translationKey) { if (name !== translationKey) {
return lang(langKey, name); return oldLang(langKey, name);
} }
const translatedNames = new Intl.DisplayNames([language], { type: 'language' }); const translatedNames = new Intl.DisplayNames([language], { type: 'language' });
const translatedName = translatedNames.of(langCode)!; const translatedName = translatedNames.of(langCode)!;
return lang(`${langKey}Other`, translatedName); return oldLang(`${langKey}Other`, translatedName);
}, [language, lang]); }, [language, oldLang]);
const buttonText = useMemo(() => { const buttonText = useMemo(() => {
if (isTranslating) return lang('ShowOriginalButton'); if (isTranslating) return oldLang('ShowOriginalButton');
return getTextWithLanguage('TranslateToButton', translationLanguage); return getTextWithLanguage('TranslateToButton', translationLanguage);
}, [translationLanguage, getTextWithLanguage, isTranslating, lang]); }, [translationLanguage, getTextWithLanguage, isTranslating, oldLang]);
const doNotTranslateText = useMemo(() => { const doNotTranslateText = useMemo(() => {
if (!detectedChatLanguage) return undefined; if (!detectedChatLanguage) return undefined;
@ -270,6 +278,10 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
openChatLanguageModal({ chatId }); openChatLanguageModal({ chatId });
}); });
const handleCocoonClick = useLastCallback(() => {
openCocoonModal();
});
const handleDoNotTranslate = useLastCallback(() => { const handleDoNotTranslate = useLastCallback(() => {
if (!detectedChatLanguage) return; if (!detectedChatLanguage) return;
@ -294,11 +306,11 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
size="smaller" size="smaller"
className={isOpen ? 'active' : ''} className={isOpen ? 'active' : ''}
onClick={onTrigger} onClick={onTrigger}
ariaLabel={lang('TranslateMessage')} ariaLabel={oldLang('TranslateMessage')}
iconName="language" iconName="language"
/> />
); );
}, [isRightColumnShown, lang]); }, [isRightColumnShown, oldLang]);
return ( return (
<div className="HeaderActions"> <div className="HeaderActions">
@ -312,12 +324,28 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
{buttonText} {buttonText}
</MenuItem> </MenuItem>
<MenuItem icon="replace" onClick={handleChangeLanguage}> <MenuItem icon="replace" onClick={handleChangeLanguage}>
{lang('Chat.Translate.Menu.To')} {oldLang('Chat.Translate.Menu.To')}
</MenuItem> </MenuItem>
<MenuSeparator /> <MenuSeparator />
{detectedChatLanguage {detectedChatLanguage
&& <MenuItem icon="hand-stop" onClick={handleDoNotTranslate}>{doNotTranslateText}</MenuItem>} && <MenuItem icon="hand-stop" onClick={handleDoNotTranslate}>{doNotTranslateText}</MenuItem>}
<MenuItem icon="close-circle" onClick={handleHide}>{lang('Hide')}</MenuItem> <MenuItem icon="close-circle" onClick={handleHide}>{oldLang('Hide')}</MenuItem>
<MenuSeparator />
<MenuItem withWrap onClick={handleCocoonClick}>
{lang('TranslateMenuCocoon', {
link: (
<Link isPrimary onClick={(e) => e.preventDefault()}>
{lang('TranslateMenuCocoonLinkText')}
</Link>
),
}, {
withNodes: true,
withMarkdown: true,
specialReplacement: {
'🥚': <CustomEmoji documentId={COCOON_EMOJI_ID} />,
},
})}
</MenuItem>
</DropdownMenu> </DropdownMenu>
)} )}
{!isMobile && ( {!isMobile && (
@ -329,7 +357,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
fluid fluid
onClick={handleSubscribeClick} onClick={handleSubscribeClick}
> >
{lang(isChannel ? 'ProfileJoinChannel' : 'ProfileJoinGroup')} {oldLang(isChannel ? 'ProfileJoinChannel' : 'ProfileJoinGroup')}
</Button> </Button>
)} )}
{canExpandActions && shouldSendJoinRequest && ( {canExpandActions && shouldSendJoinRequest && (
@ -339,7 +367,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
fluid fluid
onClick={handleSubscribeClick} onClick={handleSubscribeClick}
> >
{lang('ChannelJoinRequest')} {oldLang('ChannelJoinRequest')}
</Button> </Button>
)} )}
{canExpandActions && canStartBot && ( {canExpandActions && canStartBot && (
@ -349,7 +377,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
fluid fluid
onClick={handleStartBot} onClick={handleStartBot}
> >
{lang('BotStart')} {oldLang('BotStart')}
</Button> </Button>
)} )}
{canExpandActions && canRestartBot && ( {canExpandActions && canRestartBot && (
@ -359,7 +387,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
fluid fluid
onClick={handleRestartBot} onClick={handleRestartBot}
> >
{lang('BotRestart')} {oldLang('BotRestart')}
</Button> </Button>
)} )}
{canExpandActions && canUnblock && ( {canExpandActions && canUnblock && (
@ -369,7 +397,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
fluid fluid
onClick={handleUnblock} onClick={handleUnblock}
> >
{lang('Unblock')} {oldLang('Unblock')}
</Button> </Button>
)} )}
{canSearch && ( {canSearch && (
@ -379,7 +407,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
color="translucent" color="translucent"
size="smaller" size="smaller"
onClick={handleSearchClick} onClick={handleSearchClick}
ariaLabel={lang('Conversation.SearchPlaceholder')} ariaLabel={oldLang('Conversation.SearchPlaceholder')}
iconName="search" iconName="search"
/> />
)} )}
@ -404,7 +432,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
size="smaller" size="smaller"
iconName="user" iconName="user"
onClick={handleJoinRequestsClick} onClick={handleJoinRequestsClick}
ariaLabel={isChannel ? lang('SubscribeRequests') : lang('MemberRequests')} ariaLabel={isChannel ? oldLang('SubscribeRequests') : oldLang('MemberRequests')}
> >
<div className="badge">{pendingJoinRequests}</div> <div className="badge">{pendingJoinRequests}</div>
</Button> </Button>

View File

@ -583,13 +583,13 @@
&:not(.custom-shape) .text-content { &:not(.custom-shape) .text-content {
.emoji { .emoji {
width: calc(1.25 * var(--message-text-size, 1rem)); width: 1.25em;
height: calc(1.25 * var(--message-text-size, 1rem)); height: 1.25em;
background-size: calc(1.25 * var(--message-text-size, 1rem)); background-size: 1.25em;
} }
.custom-emoji { .custom-emoji {
--custom-emoji-size: max(calc(1.25 * var(--message-text-size, 1rem)), 20px); --custom-emoji-size: max(1.25em, 20px);
width: var(--custom-emoji-size); width: var(--custom-emoji-size);
height: var(--custom-emoji-size); height: var(--custom-emoji-size);

View File

@ -15,6 +15,7 @@ import BirthdaySetupModal from './birthday/BirthdaySetupModal.async';
import BoostModal from './boost/BoostModal.async'; import BoostModal from './boost/BoostModal.async';
import ChatInviteModal from './chatInvite/ChatInviteModal.async'; import ChatInviteModal from './chatInvite/ChatInviteModal.async';
import ChatlistModal from './chatlist/ChatlistModal.async'; import ChatlistModal from './chatlist/ChatlistModal.async';
import CocoonModal from './cocoon/CocoonModal.async';
import CollectibleInfoModal from './collectible/CollectibleInfoModal.async'; import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
import DeleteAccountModal from './deleteAccount/DeleteAccountModal.async'; import DeleteAccountModal from './deleteAccount/DeleteAccountModal.async';
import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async'; import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async';
@ -130,7 +131,8 @@ type ModalKey = keyof Pick<TabState,
'storyStealthModal' | 'storyStealthModal' |
'isPasskeyModalOpen' | 'isPasskeyModalOpen' |
'birthdaySetupModal' | 'birthdaySetupModal' |
'isQuickChatPickerOpen' 'isQuickChatPickerOpen' |
'isCocoonModalOpen'
>; >;
type StateProps = { type StateProps = {
@ -207,6 +209,7 @@ const MODALS: ModalRegistry = {
isPasskeyModalOpen: PasskeyModal, isPasskeyModalOpen: PasskeyModal,
birthdaySetupModal: BirthdaySetupModal, birthdaySetupModal: BirthdaySetupModal,
isQuickChatPickerOpen: QuickChatPickerModal, isQuickChatPickerOpen: QuickChatPickerModal,
isCocoonModalOpen: CocoonModal,
}; };
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>; const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -0,0 +1,14 @@
import type { OwnProps } from './CocoonModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const CocoonModalAsync = (props: OwnProps) => {
const { modal } = props;
const CocoonModal = useModuleLoader(Bundles.Extra, 'CocoonModal', !modal);
return CocoonModal ? <CocoonModal {...props} /> : undefined;
};
export default CocoonModalAsync;

View File

@ -0,0 +1,26 @@
.header {
position: relative;
margin-top: -1rem;
margin-inline: -1rem;
}
.footer {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.footerText {
font-size: 0.9375rem;
color: var(--color-text-secondary);
text-align: center;
text-wrap: balance;
}
.egg {
transition: transform 0.25s ease-out;
&:hover {
transform: scale(1.1);
}
}

View File

@ -0,0 +1,108 @@
import { memo, useMemo } from '@teact';
import { getActions } from '../../../global';
import type { TabState } from '../../../global/types';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import SafeLink from '../../common/SafeLink';
import Button from '../../ui/Button';
import Link from '../../ui/Link';
import ParticlesHeader from '../common/ParticlesHeader';
import TableAboutModal, { type TableAboutData } from '../common/TableAboutModal';
import styles from './CocoonModal.module.scss';
export type OwnProps = {
modal: TabState['isCocoonModalOpen'];
};
const CocoonModal = ({ modal }: OwnProps) => {
const { closeCocoonModal, openTelegramLink } = getActions();
const isOpen = Boolean(modal);
const lang = useLang();
const handleClose = useLastCallback(() => {
closeCocoonModal();
});
const openLinkAndClose = useLastCallback((url: string) => {
openTelegramLink({ url });
handleClose();
});
const listItemData = useMemo(() => {
const feature1Description = lang('CocoonFeature1Text', {
username: (
<Link
isPrimary
onClick={() => {
openLinkAndClose(lang('CocoonFeature1UsernameLink'));
}}
>
{lang('CocoonFeature1Username')}
</Link>
),
}, { withNodes: true, withMarkdown: true });
const feature3Description = lang('CocoonFeature3Text', {
link: <SafeLink url={lang('CocoonFeature3Link')} text={lang('CocoonFeature3LinkText')} />,
}, {
withNodes: true,
withMarkdown: true,
});
return [
['lock', lang('CocoonFeature1Title'), feature1Description],
['stats', lang('CocoonFeature2Title'), lang('CocoonFeature2Text')],
['gift', lang('CocoonFeature3Title'), feature3Description],
] satisfies TableAboutData;
}, [lang]);
const header = useMemo(() => {
return (
<ParticlesHeader
className={styles.header}
modelClassName={styles.egg}
model="ai-egg"
color="purple"
title={lang('CocoonTitle')}
description={lang('CocoonDescription', undefined, { withNodes: true, withMarkdown: true })}
/>
);
}, [lang]);
const footer = useMemo(() => {
return (
<div className={styles.footer}>
<span className={styles.footerText}>
{lang('CocoonFooterText', {
link: (
<Link isPrimary onClick={() => openLinkAndClose(lang('CocoonFooterLink'))}>
{lang('CocoonFooterLinkText')}
</Link>
),
}, { withNodes: true, withMarkdown: true })}
</span>
<Button iconName="understood" onClick={handleClose}>
{lang('ButtonUnderstood')}
</Button>
</div>
);
}, [lang]);
return (
<TableAboutModal
isOpen={isOpen}
onClose={closeCocoonModal}
listItemData={listItemData}
withSeparator
header={header}
footer={footer}
/>
);
};
export default memo(CocoonModal);

View File

@ -5,6 +5,29 @@
padding-top: 9rem; padding-top: 9rem;
} }
.ai-egg {
padding-top: 10rem;
background-color: #061029;
background-image: radial-gradient(circle at center, #FFFFFF08 0%, #00000000 50%);
.title {
margin-bottom: 0;
font-size: 2.5rem;
color: transparent;
background-image: linear-gradient(90deg, #67c9ff 0%, #cb56ff 100%);
background-clip: text;
}
.description {
color: #b8c9ef;
:global(b) {
color: white;
}
}
}
.particles { .particles {
position: absolute; position: absolute;
top: 0; top: 0;
@ -41,3 +64,10 @@
transform: scale(1.1); transform: scale(1.1);
} }
} }
.cocoon {
position: absolute;
z-index: 1;
top: 1.75rem;
height: 8.125rem;
}

View File

@ -15,14 +15,17 @@ import SwayingStar from './SwayingStar.tsx';
import styles from './ParticlesHeader.module.scss'; import styles from './ParticlesHeader.module.scss';
import Cocoon from '../../../assets/cocoon.webp';
interface OwnProps { interface OwnProps {
model: 'swaying-star' | 'speeding-diamond' | 'sticker'; model: 'swaying-star' | 'speeding-diamond' | 'ai-egg' | 'sticker';
sticker?: ApiSticker; sticker?: ApiSticker;
color: 'purple' | 'gold' | 'blue'; color: 'purple' | 'gold' | 'blue';
title: TeactNode; title: TeactNode;
description: TeactNode; description: TeactNode;
isDisabled?: boolean; isDisabled?: boolean;
className?: string; className?: string;
modelClassName?: string;
} }
const GIFT_STICKER_SIZE = 8 * REM; const GIFT_STICKER_SIZE = 8 * REM;
@ -39,6 +42,7 @@ function ParticlesHeader({
description, description,
isDisabled, isDisabled,
className, className,
modelClassName,
}: OwnProps) { }: OwnProps) {
const stickerRef = useRef<HTMLDivElement>(); const stickerRef = useRef<HTMLDivElement>();
const triggerSparklesRef = useRef<(() => void) | undefined>(); const triggerSparklesRef = useRef<(() => void) | undefined>();
@ -52,7 +56,7 @@ function ParticlesHeader({
}); });
return ( return (
<div className={buildClassName(styles.root, className)}> <div className={buildClassName(styles.root, styles[model], className)}>
<InteractiveSparkles <InteractiveSparkles
color={color} color={color}
centerShift={PARTICLE_PARAMS.centerShift} centerShift={PARTICLE_PARAMS.centerShift}
@ -63,16 +67,27 @@ function ParticlesHeader({
{model === 'swaying-star' ? ( {model === 'swaying-star' ? (
<SwayingStar <SwayingStar
className={modelClassName}
color={color as 'purple' | 'gold'} color={color as 'purple' | 'gold'}
centerShift={PARTICLE_PARAMS.centerShift} centerShift={PARTICLE_PARAMS.centerShift}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
/> />
) : model === 'ai-egg' ? (
<img
src={Cocoon}
alt=""
role="presentation"
aria-hidden="true"
className={buildClassName(styles.cocoon, modelClassName)}
draggable={false}
onMouseMove={handleMouseMove}
/>
) : model === 'speeding-diamond' ? ( ) : model === 'speeding-diamond' ? (
<SpeedingDiamond onMouseMove={handleMouseMove} /> <SpeedingDiamond className={modelClassName} onMouseMove={handleMouseMove} />
) : model === 'sticker' && sticker && ( ) : model === 'sticker' && sticker && (
<div <div
ref={stickerRef} ref={stickerRef}
className={styles.stickerWrapper} className={buildClassName(styles.stickerWrapper, modelClassName)}
style={`width: ${GIFT_STICKER_SIZE}px; height: ${GIFT_STICKER_SIZE}px`} style={`width: ${GIFT_STICKER_SIZE}px; height: ${GIFT_STICKER_SIZE}px`}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
> >

View File

@ -2,6 +2,7 @@ import { memo, useState } from '@teact';
import { requestMutation } from '../../../lib/fasterdom/fasterdom.ts'; import { requestMutation } from '../../../lib/fasterdom/fasterdom.ts';
import { animateSingle } from '../../../util/animation.ts'; import { animateSingle } from '../../../util/animation.ts';
import buildClassName from '../../../util/buildClassName';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets.ts'; import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets.ts';
import useLastCallback from '../../../hooks/useLastCallback.ts'; import useLastCallback from '../../../hooks/useLastCallback.ts';
@ -13,6 +14,7 @@ import styles from './SpeedingDiamond.module.scss';
import diamondPreviewUrl from '../../../assets/diamond.png'; import diamondPreviewUrl from '../../../assets/diamond.png';
interface OwnProps { interface OwnProps {
className?: string;
onMouseMove: NoneToVoidFunction; onMouseMove: NoneToVoidFunction;
} }
@ -24,7 +26,7 @@ const SLOWDOWN_DURATION = 1500;
let slowdownTimeout: number | undefined; let slowdownTimeout: number | undefined;
let isAnimating = true; let isAnimating = true;
function SpeedingDiamond({ onMouseMove }: OwnProps) { function SpeedingDiamond({ className, onMouseMove }: OwnProps) {
const [speed, setSpeed] = useState(MIN_SPEED); const [speed, setSpeed] = useState(MIN_SPEED);
const handleMouseMove = useLastCallback(() => { const handleMouseMove = useLastCallback(() => {
@ -58,7 +60,7 @@ function SpeedingDiamond({ onMouseMove }: OwnProps) {
}); });
return ( return (
<div className={styles.root}> <div className={buildClassName(styles.root, className)}>
<div <div
className={styles.diamond} className={styles.diamond}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}

View File

@ -8,6 +8,7 @@ import useLastCallback from '../../../hooks/useLastCallback.ts';
import styles from './SwayingStar.module.scss'; import styles from './SwayingStar.module.scss';
interface OwnProps { interface OwnProps {
className?: string;
color: 'purple' | 'gold'; color: 'purple' | 'gold';
centerShift: readonly [number, number]; centerShift: readonly [number, number];
onMouseMove: NoneToVoidFunction; onMouseMove: NoneToVoidFunction;
@ -16,6 +17,7 @@ interface OwnProps {
const INTERACTIVE_RADIUS = 50; const INTERACTIVE_RADIUS = 50;
function SwayingStar({ function SwayingStar({
className,
color, color,
centerShift, centerShift,
onMouseMove, onMouseMove,
@ -48,7 +50,7 @@ function SwayingStar({
return ( return (
<div <div
className={styles.root} className={buildClassName(styles.root, className)}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >

View File

@ -75,8 +75,10 @@
align-items: center; align-items: center;
} }
.listItem { .listItems {
padding-bottom: 0.5rem; display: flex;
flex-direction: column;
gap: 0.5rem;
} }
.listItemTitle { .listItemTitle {
@ -86,5 +88,5 @@
.separator { .separator {
width: calc(100% + 2rem); // Hack to cover modal paddings width: calc(100% + 2rem); // Hack to cover modal paddings
margin-block: 1rem; margin-bottom: 0.5rem;
} }

View File

@ -63,7 +63,7 @@ const TableAboutModal = ({
</div> </div>
)} )}
{header} {header}
<div> <div className={styles.listItems}>
{listItemData?.map(([icon, title, subtitle]) => { {listItemData?.map(([icon, title, subtitle]) => {
return ( return (
<ListItem <ListItem

View File

@ -1,6 +1,3 @@
import type { FC } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
@ -16,9 +13,9 @@ type OwnProps = {
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void; onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}; };
const Link: FC<OwnProps> = ({ const Link = ({
children, isPrimary, className, isRtl, withMultilineFix, onClick, children, isPrimary, className, isRtl, withMultilineFix, onClick,
}) => { }: OwnProps) => {
const handleClick = useLastCallback((e: React.MouseEvent<HTMLAnchorElement>) => { const handleClick = useLastCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault(); e.preventDefault();
onClick!(e); onClick!(e);

View File

@ -161,11 +161,20 @@
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
} }
a:hover {
text-decoration: none;
}
&.wrap { &.wrap {
display: block; display: block;
white-space: normal; white-space: normal;
} }
&.text-only {
padding-inline: 0.5rem;
line-height: 1.375;
}
&.menu-custom-emoji-sets { &.menu-custom-emoji-sets {
margin: 0 0.25rem; margin: 0 0.25rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;

View File

@ -91,6 +91,7 @@ const MenuItem = (props: MenuItemProps) => {
destructive && 'destructive', destructive && 'destructive',
!isTouchScreen && 'compact', !isTouchScreen && 'compact',
withWrap && 'wrap', withWrap && 'wrap',
!icon && !customIcon && 'text-only',
); );
const content = ( const content = (

View File

@ -345,6 +345,7 @@ export const REPLIES_USER_ID = '1271266957'; // TODO For Test connection ID must
export const VERIFICATION_CODES_USER_ID = '489000'; export const VERIFICATION_CODES_USER_ID = '489000';
export const ANONYMOUS_USER_ID = '2666000'; export const ANONYMOUS_USER_ID = '2666000';
export const RESTRICTED_EMOJI_SET_ID = '7173162320003080'; export const RESTRICTED_EMOJI_SET_ID = '7173162320003080';
export const COCOON_EMOJI_ID = '5197252827247841976';
export const LOCAL_MESSAGES_LIMIT = 1e6; // 1M export const LOCAL_MESSAGES_LIMIT = 1e6; // 1M
export const CHANNEL_ID_BASE = 10n ** 12n; export const CHANNEL_ID_BASE = 10n ** 12n;
export const DEFAULT_GIF_SEARCH_BOT_USERNAME = 'gif'; export const DEFAULT_GIF_SEARCH_BOT_USERNAME = 'gif';

View File

@ -961,3 +961,12 @@ addActionHandler('openQuickChatPicker', (global, actions, payload): ActionReturn
}); });
addTabStateResetterAction('closeQuickChatPicker', 'isQuickChatPickerOpen'); addTabStateResetterAction('closeQuickChatPicker', 'isQuickChatPickerOpen');
addActionHandler('openCocoonModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
isCocoonModalOpen: true,
}, tabId);
});
addTabStateResetterAction('closeCocoonModal', 'isCocoonModalOpen');

View File

@ -2996,6 +2996,9 @@ export interface ActionPayloads {
chatId: string; chatId: string;
messageId: number; messageId: number;
}; };
openCocoonModal: WithTabId | undefined;
closeCocoonModal: WithTabId | undefined;
} }
export interface RequiredActionPayloads { export interface RequiredActionPayloads {

View File

@ -975,4 +975,6 @@ export type TabState = {
isWaitingForStarGiftTransfer?: true; isWaitingForStarGiftTransfer?: true;
insertingPeerIdMention?: string; insertingPeerIdMention?: string;
shouldSaveAttachmentsCompression?: boolean; shouldSaveAttachmentsCompression?: boolean;
isCocoonModalOpen?: boolean;
}; };

View File

@ -1872,6 +1872,7 @@ payments.getStarsStatus#4ea9b3bf flags:# ton:flags.0?true peer:InputPeer = payme
payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus; payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus;
payments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult; payments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult;
payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates;
payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true ton:flags.1?true peer:InputPeer = payments.StarsRevenueStats;
payments.getStarsTransactionsByID#2dca16b8 flags:# ton:flags.0?true peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus; payments.getStarsTransactionsByID#2dca16b8 flags:# ton:flags.0?true peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>; payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>;
payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus; payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus;

View File

@ -331,6 +331,7 @@
"payments.getStarsTransactions", "payments.getStarsTransactions",
"payments.getStarsTransactionsByID", "payments.getStarsTransactionsByID",
"payments.getStarsSubscriptions", "payments.getStarsSubscriptions",
"payments.getStarsRevenueStats",
"payments.changeStarsSubscription", "payments.changeStarsSubscription",
"payments.fulfillStarsSubscription", "payments.fulfillStarsSubscription",
"payments.sendStarsForm", "payments.sendStarsForm",

View File

@ -1916,6 +1916,19 @@ export interface LangPair {
'SettingsDataClearMediaCache': undefined; 'SettingsDataClearMediaCache': undefined;
'SettingsDataClearMediaCacheDescription': undefined; 'SettingsDataClearMediaCacheDescription': undefined;
'SettingsDataClearMediaDone': undefined; 'SettingsDataClearMediaDone': undefined;
'TranslateMenuCocoonLinkText': undefined;
'CocoonTitle': undefined;
'CocoonDescription': undefined;
'CocoonFeature1Title': undefined;
'CocoonFeature1Username': undefined;
'CocoonFeature1UsernameLink': undefined;
'CocoonFeature2Title': undefined;
'CocoonFeature2Text': undefined;
'CocoonFeature3Title': undefined;
'CocoonFeature3LinkText': undefined;
'CocoonFeature3Link': undefined;
'CocoonFooterLinkText': undefined;
'CocoonFooterLink': undefined;
'ChatListAuctionWinning': undefined; 'ChatListAuctionWinning': undefined;
'ChatListAuctionOutbid': undefined; 'ChatListAuctionOutbid': undefined;
'ChatListAuctionView': undefined; 'ChatListAuctionView': undefined;
@ -3360,6 +3373,18 @@ export interface LangPairWithVariables<V = LangVariable> {
'status': V; 'status': V;
'onlineCount': V; 'onlineCount': V;
}; };
'TranslateMenuCocoon': {
'link': V;
};
'CocoonFeature1Text': {
'username': V;
};
'CocoonFeature3Text': {
'link': V;
};
'CocoonFooterText': {
'link': V;
};
'ChatListAuctionMixed': { 'ChatListAuctionMixed': {
'winCount': V; 'winCount': V;
'outbidCount': V; 'outbidCount': V;

View File

@ -1,5 +1,3 @@
import type React from '../lib/teact/teact';
const stopEvent = (e: React.UIEvent | Event | React.FormEvent) => { const stopEvent = (e: React.UIEvent | Event | React.FormEvent) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();

View File

@ -164,7 +164,7 @@ export default function createConfig(
], ],
}, },
{ {
test: /\.(woff(2)?|ttf|eot|svg|png|jpg|tgs)(\?v=\d+\.\d+\.\d+)?$/, test: /\.(woff(2)?|ttf|eot|svg|png|jpg|tgs|webp)(\?v=\d+\.\d+\.\d+)?$/,
type: 'asset/resource', type: 'asset/resource',
}, },
{ {